changeset 500:0742f421caba experimental-inline

Merged revisions 487-603 via svnmerge from http://svn.edgewall.org/repos/genshi/trunk
author cmlenz
date Fri, 01 Jun 2007 17:21:47 +0000
parents 869b7885a516
children 1837f39efd6f
files COPYING ChangeLog MANIFEST.in UPGRADE.txt doc/2000ft.graffle doc/2000ft.png doc/builder.txt doc/epydoc.conf doc/filters.txt doc/index.txt doc/plugin.txt doc/streams.txt doc/style/edgewall.css doc/style/epydoc.css doc/templates.txt doc/text-templates.txt doc/xml-templates.txt examples/bench/basic.py examples/bench/bigtable.py examples/bench/django/templates/base.html examples/bench/django/templates/template.html examples/cherrypy/index.py examples/turbogears/genshitest/controllers.py examples/turbogears/genshitest/templates/master.html examples/turbogears/genshitest/templates/welcome.html genshi/__init__.py genshi/builder.py genshi/core.py genshi/filters.py genshi/filters/__init__.py genshi/filters/html.py genshi/filters/i18n.py genshi/filters/tests/__init__.py genshi/filters/tests/html.py genshi/filters/tests/i18n.py genshi/input.py genshi/output.py genshi/path.py genshi/template/__init__.py genshi/template/base.py genshi/template/core.py genshi/template/directives.py genshi/template/eval.py genshi/template/interpolation.py genshi/template/loader.py genshi/template/markup.py genshi/template/plugin.py genshi/template/tests/__init__.py genshi/template/tests/base.py genshi/template/tests/core.py genshi/template/tests/directives.py genshi/template/tests/eval.py genshi/template/tests/interpolation.py genshi/template/tests/loader.py genshi/template/tests/markup.py genshi/template/tests/plugin.py genshi/template/tests/text.py genshi/template/text.py genshi/tests/__init__.py genshi/tests/filters.py genshi/tests/input.py genshi/tests/output.py genshi/tests/path.py genshi/util.py setup.py
diffstat 65 files changed, 5293 insertions(+), 2394 deletions(-) [+]
line wrap: on
line diff
--- a/COPYING
+++ b/COPYING
@@ -1,4 +1,4 @@
-Copyright (C) 2006 Edgewall Software
+Copyright (C) 2006-2007 Edgewall Software
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,54 @@
+Version 0.5
+http://svn.edgewall.org/repos/genshi/tags/0.5.0/
+(?, from branches/stable/0.5.x)
+
+ * Added #include directive for text templates (ticket #115).
+
+
+Version 0.4.2
+http://svn.edgewall.org/repos/genshi/tags/0.4.2/
+(?, from branches/stable/0.4.x)
+
+ * The `doctype` parameter of the markup serializers now also accepts the "name"
+   of the doctype as string, in addition to the `(name, pubid, sysid)` tuple.
+ * The I18n filter was not replacing the original attributes with the
+   translation, but instead adding a second attribute with the same name.
+
+
+Version 0.4.1
+http://svn.edgewall.org/repos/genshi/tags/0.4.1/
+(May 21 2007, from branches/stable/0.4.x)
+
+ * Fix incorrect reference to translation function in the I18N filter.
+ * The `ET()` function now correctly handles attributes with a namespace.
+ * XML declarations are now processed internally, as well as written to the
+   output when XML serialization is used (ticket #111).
+ * Added the functions `encode()` and `get_serializer()` to the `genshi.output`
+   module, which provide a lower-level API to the functionality previously only
+   available through `Stream.render()` and `Stream.serialize()`.
+ * The `DocType` class now has a `get(name)` function that returns a `DOCTYPE`
+   tuple for a given string.
+ * Added frameset variants to the `DocType` constants for HTML 4.01 and XHTML
+   1.0.
+ * Improved I18n extraction for pluralizable messages: for any translation
+   function with multiple string arguments (such as ``ngettext``), a single
+   item with a tuple of strings is yielded, instead an item for each string
+   argument.
+ * The `HTMLFormFiller` stream filter no longer alters form elements for which
+   the data element contains no corresponding item.
+ * Code in `<?python ?>` processing instructions no longer gets the special
+   treatment as Python code in template expressions, i.e. item and attribute
+   access are no longer interchangeable (which was broken in a number of ways
+   anyway, see ticket #113). This change does not affect expressions.
+ * Numerous fixes for the execution of Python code in `<?python ?>` processing
+   instructions (tickets #113 and #114).
+ * The `py:def` (and `#def`) directive now supports "star args" (i.e. `*args`
+   and `**kwargs`) in the function declaration (ticket #116).
+
+
 Version 0.4
 http://svn.edgewall.org/repos/genshi/tags/0.4.0/
-(?, from branches/stable/0.4.x)
+(Apr 16 2007, from branches/stable/0.4.x)
 
  * New example applications for CherryPy and web.py.
  * The template loader now uses a LRU cache to limit the number of cached
@@ -30,7 +78,39 @@
  * `MarkupTemplate`s can now be instantiated from markup streams, in addition
    to strings and file-like objects (ticket #69).
  * Improve handling of incorrectly nested tags in the HTML parser.
- * Template includes can you be nested inside fallback content.
+ * Template includes can now be nested inside fallback content.
+ * Expressions can now contain dict literals (ticket #37).
+ * It is now possible to have one or more escaped dollar signs in front of a 
+   full expression (ticket #92).
+ * The `Markup` class is now available by default in template expressions
+   (ticket #67).
+ * The handling of namespace declarations in XML/XHTML output has been improved.
+ * The `Attrs` class no longer automatically wraps all attribute names in
+   `QName` objects. This is now the responsibility of whoever is instantiating
+   `Attrs` objects (for example, stream filters and generators).
+ * Python code blocks are now supported using the `<?python ?>` processing
+   instruction (ticket #84).
+ * The way errors in template expressions are handled can now be configured. The
+   option `LenientLookup` provides the same forgiving mode used in previous
+   Genshi versions, while `StrictLookup` raises exceptions when undefined
+   variables or members are accessed. The lenient mode is still the default in
+   this version, but that may change in the future. (ticket #88)
+ * If a variable is not necessarily defined at the top level of the template
+   data, the new built-in functions `defined(key)` and `value_of(key, default)`
+   can be used so that the template also works in strict lookup mode. These
+   functions were previously only available when using Genshi via the template
+   engine plugin (for compatibility with Kid).
+ * `style` attributes are no longer allowed by the `HTMLSanitizer` by default.
+   If they are explicitly added to the set of safe attributes, any unicode
+   escapes in the attribute value are now handled properly.
+ * Namespace declarations on conditional elements (for example using a `py:if`
+   directive`) are no longer moved to the following element when the element
+   originally carrying the declaration is removed from the stream (ticket #107).
+ * Added basic built-in support for internationalizing templates by providing
+   a new `Translator` class that can both extract localizable strings from a
+   stream, and replace those strings with their localizations at render time.
+   The code for this was largely taken from previous work done by Matt Good
+   and David Fraser.
 
 
 Version 0.3.6
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,4 @@
 exclude doc/2000ft.graffle
-exclude doc/docutils.conf
 recursive-exclude doc/logo.lineform *
-exclude doc/Makefile
+include doc/api/*.*
 include doc/*.html
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -4,19 +4,23 @@
 Upgrading from Genshi 0.3.x to 0.4.x
 ------------------------------------
 
-The `genshi.template` module has been refactored into a package with
-multiple modules. While code using the normal templating APIs should
-continue to work without problems, you should make sure to remove any
-leftover traces of the `template.py` file on the installation path.
-This is not necessary when Genshi was installed as a Python egg.
+The modules ``genshi.filters`` and ``genshi.template`` have been
+refactored into packages containing multiple modules. While code using
+the regular APIs should continue to work without problems, you should
+make sure to remove any leftover traces of the ``template.py`` file on
+the installation path. This is not necessary when Genshi was installed
+as a Python egg.
 
 Results of evaluating template expressions are no longer implicitly
 called if they are callable. If you have been using that feature, you
 will need to add the parenthesis to actually call the function.
 
 Instances of `genshi.core.Attrs` are now immutable. Filters
-manipulating the attributes in a stream may need to be updated. See
-the documentation of the `Attrs` class for more information.
+manipulating the attributes in a stream may need to be updated. Also,
+the `Attrs` class no longer automatically wraps all attribute names
+in `QName` objects, so users of the `Attrs` class need to do this
+themselves. See the documentation of the `Attrs` class for more
+information.
 
 
 Upgrading from Markup
@@ -40,7 +44,7 @@
 
 has been renamed to:
 
-  `markup.template.MarkupTemplate`
+  `genshi.template.MarkupTemplate`
 
 If you've been using the Template class directly, you'll need to
 update your code (a simple find/replace should do--the API itself
--- a/doc/2000ft.graffle
+++ b/doc/2000ft.graffle
@@ -56,7 +56,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -91,7 +91,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -126,7 +126,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -161,7 +161,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -196,7 +196,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -273,7 +273,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -347,7 +347,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -423,7 +423,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -569,12 +569,12 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
 
-\f0\fs22 \cf0 Whitespace Filter}</string>
+\f0\fs22 \cf0 HTML Sanitizer}</string>
 			</dict>
 		</dict>
 		<dict>
@@ -604,12 +604,12 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
 
-\f0\fs22 \cf0 XInclude Filter}</string>
+\f0\fs22 \cf0 HTML Form Filler}</string>
 			</dict>
 		</dict>
 		<dict>
@@ -639,12 +639,12 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
 
-\f0\fs22 \cf0 HTML Sanitizer}</string>
+\f0\fs22 \cf0 I18N Translator}</string>
 			</dict>
 		</dict>
 		<dict>
@@ -720,7 +720,7 @@
 				<key>Align</key>
 				<integer>2</integer>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural
@@ -760,7 +760,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -795,7 +795,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -830,7 +830,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -911,7 +911,7 @@
 				<key>Align</key>
 				<integer>2</integer>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural
@@ -946,7 +946,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -981,7 +981,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -1062,7 +1062,7 @@
 				<key>Align</key>
 				<integer>2</integer>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural
@@ -1141,7 +1141,7 @@
 			<key>Text</key>
 			<dict>
 				<key>Text</key>
-				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420
 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
 {\colortbl;\red255\green255\blue255;}
 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
@@ -1249,7 +1249,7 @@
 		</dict>
 	</array>
 	<key>ModificationDate</key>
-	<string>2006-09-12 18:13:56 +0200</string>
+	<string>2007-04-13 15:15:25 +0200</string>
 	<key>Modifier</key>
 	<string>Christopher Lenz</string>
 	<key>NotesVisible</key>
@@ -1325,7 +1325,7 @@
 		<key>ShowStatusBar</key>
 		<true/>
 		<key>VisibleRegion</key>
-		<string>{{-121, 0}, {765, 470}}</string>
+		<string>{{-121, 0}, {765, 446}}</string>
 		<key>Zoom</key>
 		<string>1</string>
 	</dict>
index 3c6dc53915076b2fd6225b9798325463a3e7399d..d0209916dd84c14e852056736dffa991c1cc1506
GIT binary patch
literal 28698
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
S+j_ADkoqVuRweQ&=>Gv;WFyl6
deleted file mode 100644
--- a/doc/builder.txt
+++ /dev/null
@@ -1,72 +0,0 @@
-.. -*- mode: rst; encoding: utf-8 -*-
-
-==================================
-Generating Markup Programmatically
-==================================
-
-Genshi provides a ``builder`` module which lets you generate markup from Python
-code using a very simple syntax. The main entry point to the ``builder`` module
-is the ``tag`` object (which is actually an instance of the ``ElementFactory``
-class). You should rarely (if ever) need to directly import and use any of the
-other classes in the ``builder`` module.
-
-
-.. contents:: Contents
-   :depth: 2
-.. sectnum::
-
-
-Creating Elements
-=================
-
-Elements can be created through the `tag` object using attribute access, for
-example::
-
-  >>> from genshi.builder import tag
-  >>> doc = tag.p('Some text and ', tag.a('a link', href='http://example.org/'), '.')
-  >>> doc
-  <Element "p">
-
-This produces an ``Element`` instance which can be further modified to add child
-nodes and attributes. This is done by ā€œcallingā€ the element: positional
-arguments are added as child nodes (alternatively, the ``append`` method can be
-used for that purpose), whereas keywords arguments are added as attributes::
-
-  >>> doc(tag.br)
-  <Element "p">
-  >>> print doc
-  <p>Some text and <a href="http://example.org/">a link</a>.<br/></p>
-
-If an attribute name collides with a Python keyword, simply append an underscore
-to the name::
-
-  >>> doc(class_='intro')
-  <Element "p">
-  >>> print doc
-  <p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
-
-As shown above, an ``Element`` can easily be directly rendered to XML text by
-printing it or using the Python ``str()`` function. This is basically a
-shortcut for converting the ``Element`` to a stream and serializing that
-stream::
-
-  >>> stream = doc.generate()
-  >>> stream
-  <genshi.core.Stream object at ...>
-  >>> print stream
-  <p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
-
-
-Creating Fragments
-==================
-
-The ``tag`` object also allows creating ā€œfragmentsā€, which are basically lists
-of nodes (elements or text) that don't have a parent element. This can be useful
-for creating snippets of markup that are attached to a parent element later (for
-example in a template). Fragments are created by calling the ``tag`` object::
-
-  >>> fragment = tag('Hello, ', tag.em('world'), '!')
-  >>> fragment
-  <Fragment>
-  >>> print fragment
-  Hello, <em>world</em>!
new file mode 100644
--- /dev/null
+++ b/doc/epydoc.conf
@@ -0,0 +1,24 @@
+[epydoc]
+
+name: Documentation Index
+url: ../index.html
+modules: genshi
+verbosity: 1
+
+# Extraction
+docformat: restructuredtext
+parse: yes
+introspect: yes
+exclude: .*\.tests.*
+inheritance: listed
+private: no
+imports: no
+include-log: no
+
+# HTML output
+output: html
+target: doc/api/
+css: doc/style/epydoc.css
+top: genshi
+frames: no
+sourcecode: no
new file mode 100644
--- /dev/null
+++ b/doc/filters.txt
@@ -0,0 +1,132 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+==============
+Stream Filters
+==============
+
+`Markup Streams`_ showed how to write filters and how they are applied to
+markup streams. This page describes the features of the various filters that
+come with Genshi itself.
+
+.. _`Markup Streams`: streams.html
+
+.. contents:: Contents
+   :depth: 1
+.. sectnum::
+
+
+HTML Form Filler
+================
+
+The filter ``genshi.filters.HTMLFormFiller`` can automatically populate an HTML
+form from values provided as a simple dictionary. When using thi filter, you can
+basically omit any ``value``, ``selected``, or ``checked`` attributes from form
+controls in your templates, and let the filter do all that work for you.
+
+``HTMLFormFiller`` takes a dictionary of data to populate the form with, where
+the keys should match the names of form elements, and the values determine the
+values of those controls. For example::
+
+  >>> from genshi.filters import HTMLFormFiller
+  >>> from genshi.template import MarkupTemplate
+  >>> template = MarkupTemplate("""<form>
+  ...   <p>
+  ...     <label>User name:
+  ...       <input type="text" name="username" />
+  ...     </label><br />
+  ...     <label>Password:
+  ...       <input type="password" name="password" />
+  ...     </label><br />
+  ...     <label>
+  ...       <input type="checkbox" name="remember" /> Remember me
+  ...     </label>
+  ...   </p>
+  ... </form>""")
+  >>> filler = HTMLFormFiller(data=dict(username='john', remember=True))
+  >>> print template.generate() | filler
+  <form>
+    <p>
+      <label>User name:
+        <input type="text" name="username" value="john"/>
+      </label><br/>
+      <label>Password:
+        <input type="password" name="password"/>
+      </label><br/>
+      <label>
+        <input type="checkbox" name="remember" checked="checked"/> Remember me
+      </label>
+    </p>
+  </form>
+
+.. note:: This processing is done without in any way reparsing the template
+          output. As any stream filter it operates after the template output is
+          generated but *before* that output is actually serialized.
+
+The filter will of course also handle radio buttons as well as ``<select>`` and
+``<textarea>`` elements. For radio buttons to be marked as checked, the value in
+the data dictionary needs to match the ``value`` attribute of the ``<input>``
+element, or evaluate to a truth value if the element has no such attribute. For
+options in a ``<select>`` box to be marked as selected, the value in the data
+dictionary needs to match the ``value`` attribute of the ``<option>`` element,
+or the text content of the option if it has no ``value`` attribute. Password and
+file input fields are not populated, as most browsers would ignore that anyway
+for security reasons.
+
+You'll want to make sure that the values in the data dictionary have already
+been converted to strings. While the filter may be able to deal with non-string
+data in some cases (such as check boxes), in most cases it will either not
+attempt any conversion or not produce the desired results.
+
+You can restrict the form filler to operate only on a specific ``<form>`` by
+passing either the ``id`` or the ``name`` keyword argument to the initializer.
+If either of those is specified, the filter will only apply to form tags with
+an attribute matching the specified value.
+
+
+HTML Sanitizer
+==============
+
+The filter ``genshi.filters.HTMLSanitizer`` filter can be used to clean up
+user-submitted HTML markup, removing potentially dangerous constructs that could
+be used for various kinds of abuse, such as cross-site scripting (XSS) attacks::
+
+  >>> from genshi.filters import HTMLSanitizer
+  >>> from genshi.input import HTML
+  >>> html = HTML("""<div>
+  ...   <p>Innocent looking text.</p>
+  ...   <script>alert("Danger: " + document.cookie)</script>
+  ... </div>""")
+  >>> sanitize = HTMLSanitizer()
+  >>> print html | sanitize
+  <div>
+    <p>Innocent looking text.</p>
+  </div>
+
+In this example, the ``<script>`` tag was removed from the output.
+
+You can determine which tags and attributes should be allowed by initializing
+the filter with corresponding sets. See the API documentation for more
+information.
+
+Inline ``style`` attributes are forbidden by default. If you allow them, the
+filter will still perform sanitization on the contents any encountered inline
+styles: the proprietary ``expression()`` function (supported only by Internet
+Explorer) is removed, and any property using an ``url()`` which a potentially
+dangerous URL scheme (such as ``javascript:``) are also stripped out::
+
+  >>> from genshi.filters import HTMLSanitizer
+  >>> from genshi.input import HTML
+  >>> html = HTML("""<div>
+  ...   <br style="background: url(javascript:alert(document.cookie); color: #000" />
+  ... </div>""")
+  >>> sanitize = HTMLSanitizer(safe_attrs=HTMLSanitizer.SAFE_ATTRS | set(['style']))
+  >>> print html | sanitize
+  <div>
+    <br style="color: #000"/>
+  </div>
+
+.. warning:: You should probably not rely on the ``style`` filtering, as
+             sanitizing mixed HTML, CSS, and Javascript is very complicated and
+             suspect to various browser bugs. If you can somehow get away with
+             not allowing inline styles in user-submitted content, that would
+             definitely be the safer route to follow.
--- a/doc/index.txt
+++ b/doc/index.txt
@@ -21,7 +21,10 @@
 which is heavily inspired by Kid.
 
 * `Markup Streams <streams.html>`_
-* `Generating Markup Programmatically <builder.html>`_
-* `Genshi XML Template Language <xml-templates.html>`_
-* `Genshi Text Template Language <text-templates.html>`_
-* `Using XPath in Genshi <xpath.html>`_
+* `Templating Basics <templates.html>`_
+* `XML Template Language <xml-templates.html>`_
+* `Text Template Language <text-templates.html>`_
+* `Using Stream Filters <filters.html>`_
+* `Using XPath <xpath.html>`_
+* `Using the Templating Plugin <plugin.html>`_
+* `Generated API Documentation <api/index.html>`_
new file mode 100644
--- /dev/null
+++ b/doc/plugin.txt
@@ -0,0 +1,210 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+===========================
+Using the Templating Plugin
+===========================
+
+While you can easily use Genshi templating through the APIs provided directly
+by Genshi, in some situations you may want to use Genshi through the template
+engine plugin API. Note though that this considerably limits the power and
+flexibility of Genshi templates (for example, there's no good way to use filters
+such as Genshi's `HTMLFormFiller`_ when the plugin
+API is sitting between your code and Genshi).
+
+.. _`HTMLFormFiller`: filters.html>#html-form-filler
+
+
+.. contents:: Contents
+   :depth: 2
+.. sectnum::
+
+
+Introduction
+============
+
+Most Python web frameworks (with the notable exception of Django_) support
+a variety of different templating engines through the `Template Engine Plugin
+API`_, which was first developed by the Buffet_ and TurboGears_ projects.
+
+.. _`Template Engine Plugin API`: http://docs.turbogears.org/1.0/TemplatePlugins
+.. _`Django`: http://www.djangoproject.com/
+.. _`Buffet`: http://projects.dowski.com/projects/buffet
+.. _`TurboGears`: http://www.turbogears.org/
+
+Genshi supports this API out of the box, so you can use it in frameworks like
+TurboGears or `Pylons`_ without installing any additional packages. A small
+example TurboGears application is included in the ``examples`` directory of
+source distributions of Genshi.
+
+.. _`Pylons`: http://pylonshq.com/
+
+
+Usage
+=====
+
+The way you use Genshi through the plugin API depends very much on the framework
+you're using. In general, the approach will look something like the following:
+
+(1) Configure Genshi as the default (or an additional) template engine
+(2) Optionally specify Genshi-specific `configuration options`_
+(3) For any given *view* or *controller* (or whatever those are called in your
+    framework of choice), specify the name of the template to use and which data
+    should be made available to it.
+
+For point 1, you'll have to specify the *name* of the template engine plugin.
+For Genshi, this is **"genshi"**. However, because Genshi supports both markup
+and text templates, it also provides two separate plugins, namely
+**"genshi-markup"** and **"genshi-text"** (the "genshi" name is just an
+alias for "genshi-markup").
+
+Usually, you can choose a default template engine, but also use a different
+engine on a per-request basis. So to use markup templates in general, but a text
+template in a specific controller, you'd configure "genshi" as the default
+template engine, and specify "genshi-text" for the controllers that should use
+text templates. How exactly this works depends on the framework in use.
+
+When rendering a specific template in a controller (point 3 above), you may also
+be able to pass additional options to the plugin. This includes the ``format``
+keyword argument, which Genshi will use to override the configured default
+serialization method. In combination with specifying the "genshi-text" engine
+name as explained above, you would use this to specify the "text" serialization
+method when you want to use a text template. Or you'd specify "xml" for the
+format when you want to produce an Atom feed or other XML content.
+
+
+Extra Implicit Objects
+======================
+
+The "genshi-markup" template engine plugin adds some extra functions that are
+made available to all templates implicitly, namely:
+
+``HTML(string)``
+  Parses the given string as HTML and returns a markup stream.
+``XML(string)``
+  Parses the given string as XML and returns a markup stream.
+``ET(tree)``
+  Adapts the given `ElementTree`_ object to a markup stream.
+
+The framework may make additional objects available by default. Consult the
+documentation of your framework for more information.
+
+.. _elementtree: http://effbot.org/zone/element-index.htm
+
+
+.. _`configuration options`:
+
+Configuration Options
+=====================
+
+The plugin API allows plugins to be configured using a dictionary of strings.
+The following is a list of configuration options that Genshi supports. These may
+or may not be made available by your framework. TurboGears 1.0, for example,
+only passes a fixed set of options to all plugins.
+
+``genshi.auto_reload``
+----------------------
+Whether the template loader should check the last modification time of template 
+files, and automatically reload them if they have been changed. Specify "yes"
+to enable this reloading (which is the default), or "no" to turn it off.
+
+.. note:: You may want to disable reloading in a production environment to gain
+          a slight (and possible even negligible) improvement in loading
+          performance, but then you'll have to manually restart the server
+          process anytime the templates are updated.
+
+``genshi.default_doctype``
+--------------------------
+The default ``DOCTYPE`` declaration to use in generated markup. Valid values
+are:
+
+**html-strict** (or just **html**)
+  HTML 4.01 Strict
+**html-transitional**
+  HTML 4.01 Transitional
+**xhtml-strict** (or just **xhtml**)
+  XHTML 1.0 Strict
+**xhtml-transitional**
+  XHTML 1.0 Transitional
+**html5**
+  HTML5 (as `proposed`_ by the WHAT-WG)
+
+.. _proposed: http://www.whatwg.org/specs/web-apps/current-work/
+
+.. note:: While using the Genshi API directly allows you to specify document
+          types not in that list, the *dictionary-of-strings* based
+          configuration utilized by the plugin API unfortunately limits your
+          choices to those listed above.
+
+The default behavior is to not do any prepending/replacing of a ``DOCTYPE``, but
+rather pass through those defined in the templates (if any). If this option is
+set, however, any ``DOCTYPE`` declarations in the templates are replaced by the
+specified document type.
+
+Note that with (X)HTML, the presence and choice of the ``DOCTYPE`` can have a
+more or less dramatic impact on how modern browsers render pages that use CSS
+style sheets. In particular, browsers may switch to *quirks rendering mode* for
+certain document types, or when the ``DOCTYPE`` declaration is missing
+completely.
+
+For more information on the choice of the appropriate ``DOCTYPE``, see:
+
+* `Recommended DTDs to use in your Web document <http://www.w3.org/QA/2002/04/valid-dtd-list.html>`_
+* `Choosing a DOCTYPE <http://htmlhelp.com/tools/validator/doctype.html>`_
+
+``genshi.default_encoding``
+---------------------------
+The default output encoding to use when serializing a template. By default,
+Genshi uses UTF-8. If you need to, you can choose a different charset by
+specifying this option, although that rarely makes sense.
+
+As Genshi is not in control over what HTTP headers are being sent together with
+the template output, make sure that you (or the framework you're using)
+specify the chosen encoding as part of the outgoing ``Content-Type`` header.
+For example::
+
+  Content-Type: text/html; charset=utf-8
+
+.. note:: Browsers commonly use ISO-8859-1 by default for ``text/html``, so even
+          if you use Genshi's default UTF-8 encoding, you'll have to let the
+          browser know about that explicitly
+
+``genshi.default_format``
+-------------------------
+Determines the default serialization method to use. Valid options are:
+
+**xml**
+  Serialization to XML
+**xhtml**
+  Serialization to XHTML in a way that should be compatible with HTML (i.e. the
+  result can be sent using the ``text/html`` MIME type, but can also be handled
+  by XML parsers if you're careful).
+**html**
+  Serialization to HTML
+**text**
+  Plain text serialization
+
+See `Understanding HTML, XML and XHTML`_ for an excellent description of the
+subtle differences between the three different markup serialization options. As
+a general recommendation, if you don't have a special requirement to produce
+well-formed XML, you should probably use the **html** option for your web sites.
+
+.. _`Understanding HTML, XML and XHTML`: http://webkit.org/blog/?p=68
+
+``genshi.lookup_errors``
+------------------------
+The error handling style to use in template expressions. Can be either
+**lenient** (the default) or **strict**. See the `Error Handling`_ section for
+detailled information on the differences between these two modes.
+
+.. _`Error Handling`: templates.html#template-expressions-and-code-blocks
+
+``genshi.max_cache_size``
+-------------------------
+The maximum number of templates that the template loader will cache in memory.
+The default value is **25**. You may want to choose a higher value if your web
+site uses a larger number of templates, and you have enough memory to spare.
+
+``genshi.search_path``
+----------------------
+A colon-separated list of file-system path names that the template loader should
+use to search for templates.
--- a/doc/streams.txt
+++ b/doc/streams.txt
@@ -18,9 +18,8 @@
 A stream can be attained in a number of ways. It can be:
 
 * the result of parsing XML or HTML text, or
-* programmatically generated, or
-* the result of selecting a subset of another stream filtered by an XPath
-  expression.
+* the result of selecting a subset of another stream using XPath, or
+* programmatically generated.
 
 For example, the functions ``XML()`` and ``HTML()`` can be used to convert
 literal XML or HTML text to a markup stream::
@@ -91,7 +90,9 @@
 ``genshi.filters``. It processes a stream of HTML markup, and strips out any
 potentially dangerous constructs, such as Javascript event handlers.
 ``HTMLSanitizer`` is not a function, but rather a class that implements
-``__call__``, which means instances of the class are callable.
+``__call__``, which means instances of the class are callable::
+
+  stream = stream | HTMLSanitizer()
 
 Both the ``filter()`` method and the pipe operator allow easy chaining of
 filters::
@@ -103,15 +104,22 @@
 
   stream = stream | noop | HTMLSanitizer()
 
+For more information about the built-in filters, see `Stream Filters`_.
+
+.. _`Stream Filters`: filters.html
+
 
 Serialization
 =============
 
-The ``Stream`` class provides two methods for serializing this list of events:
-``serialize()`` and ``render()``. The former is a generator that yields chunks
-of ``Markup`` objects (which are basically unicode strings that are considered
-safe for output on the web). The latter returns a single string, by default
-UTF-8 encoded.
+Serialization means producing some kind of textual output from a stream of
+events, which you'll need when you want to transmit or store the results of
+generating or otherwise processing markup.
+
+The ``Stream`` class provides two methods for serialization: ``serialize()`` and
+``render()``. The former is a generator that yields chunks of ``Markup`` objects
+(which are basically unicode strings that are considered safe for output on the
+web). The latter returns a single string, by default UTF-8 encoded.
 
 Here's the output from ``serialize()``::
 
@@ -159,6 +167,35 @@
   Some text and a link.
 
 
+Serialization Options
+---------------------
+
+Both ``serialize()`` and ``render()`` support additional keyword arguments that
+are passed through to the initializer of the serializer class. The following
+options are supported by the built-in serializers:
+
+``strip_whitespace``
+  Whether the serializer should remove trailing spaces and empty lines. Defaults
+  to ``True``.
+
+  (This option is not available for serialization to plain text.)
+
+``doctype``
+  A ``(name, pubid, sysid)`` tuple defining the name, publid identifier, and
+  system identifier of a ``DOCTYPE`` declaration to prepend to the generated
+  output. If provided, this declaration will override any ``DOCTYPE``
+  declaration in the stream.
+
+  (This option is not available for serialization to plain text.)
+
+``namespace_prefixes``
+  The namespace prefixes to use for namespace that are not bound to a prefix
+  in the stream itself.
+
+  (This option is not available for serialization to HTML or plain text.)
+
+
+
 Using XPath
 ===========
 
--- a/doc/style/edgewall.css
+++ b/doc/style/edgewall.css
@@ -17,6 +17,14 @@
 hr { border: none;  border-top: 1px solid #ccb; margin: 2em 0; }
 p { margin: 0 0 1em; }
 
+table { border: 1px solid #999; border-width: 0 1px 0 0;
+  border-collapse: separate; border-spacing: 0;
+}
+table thead th { background: #999; border: 1px solid #999;; color: #fff;
+  font-weight: bold;
+}
+table td { border: 1px solid #ccc; border-width: 0 0 1px 1px; padding: .3em; }
+
 :link, :visited { text-decoration: none; border-bottom: 1px dotted #bbb;
   color: #b00;
 }
@@ -54,4 +62,6 @@
 }
 
 p.admonition-title { font-weight: bold; margin-bottom: 0; }
-div.note { font-style: italic; margin-left: 2em; margin-right: 2em; }
+div.note, div.warning { font-style: italic; margin-left: 2em;
+  margin-right: 2em;
+}
new file mode 100644
--- /dev/null
+++ b/doc/style/epydoc.css
@@ -0,0 +1,136 @@
+html { background: #4b4d4d url(../style/bkgnd_pattern.png); margin: 0;
+  padding: 1em 1em 3em;
+}
+body { background: #fff url(../style/vertbars.png) repeat-x;
+  border: 1px solid #000; color: #000; margin: 1em 0; padding: 0 1em 1em;
+}
+body, th, td {
+  font: normal small Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif;
+}
+h1, h2, h3, h4 {
+  font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif;
+  font-weight: bold; letter-spacing: -0.018em;
+}
+h1 { font-size: 19px; margin: 2em 0 .5em; }
+h2 { font-size: 16px; margin: 1.5em 0 .5em; }
+h3 { font-size: 14px; margin: 1.2em 0 .5em; }
+hr { border: none;  border-top: 1px solid #ccb; margin: 2em 0; }
+p { margin: 0 0 1em; }
+:link, :visited { text-decoration: none; border-bottom: 1px dotted #bbb;
+  color: #b00;
+}
+:link:hover, :visited:hover { background-color: #eee; color: #555; }
+
+table { border: none; border-collapse: collapse; }
+
+table.navbar { background: #000; color: #fff; margin: 2em 0 .33em; }
+table.navbar th { border: 1px solid #000; font-weight: bold; padding: 1px; }
+table.navbar :link, table.navbar :visited { border: none; color: #fff; }
+table.navbar :link:hover, table.navbar :visited:hover { background: none;
+  text-decoration: underline overline;
+}
+table.navbar th.navbar-select { background: #fff; color: #000; }
+span.breadcrumbs { color: #666; font-size: 95%; }
+h1.epydoc { border: none; color: #666;
+  font-size: x-large; margin: 1em 0 0; padding: 0;
+}
+pre.base-tree { color: #666; margin: 0; padding: 0; }
+pre.base-tree :link, pre.base-tree :visited { border: none; }
+pre.py-doctest, pre.variable, pre.rst-literal-block { background: #eee;
+  border: 1px solid #e6e6e6; color: #000; margin: 1em; padding: .25em;
+  overflow: auto;
+}
+pre.variable { margin: 0; }
+
+/* Summary tables */
+
+table.summary { margin: .5em 0; }
+table.summary tr.table-header { background: #f7f7f0; }
+table.summary td.table-header { color: #666; font-weight: bold; }
+table.summary th.group-header { background: #f7f7f0; color: #666;
+  font-size: 90%; font-weight: bold; text-align: left;
+}
+table.summary th, table.summary td { border: 1px solid #d7d7d7; }
+table.summary th th, table.summary td td { border: none; }
+table.summary td.summary table td { color: #666; font-size: 90%; }
+table.summary td.summary table br { display: none; }
+table.summary td.summary span.summary-type { font-family: monospace;
+  font-size: 90%;
+}
+table.summary td.summary span.summary-type code { font-size: 110%; }
+p.indent-wrapped-lines { color: #999; font-size: 85%; margin: 0;
+  padding: 0 0 0 7em; text-indent: -7em;
+}
+p.indent-wrapped-lines code { color: #999; font-size: 115%; }
+p.indent-wrapped-lines :link, p.indent-wrapped-lines :visited { border: none; }
+.summary-sig { display: block; font-family: monospace; font-size: 120%;
+  margin-bottom: .5em;
+}
+.summary-sig-name { font-weight: bold; }
+.summary-sig-arg { color: #333; }
+.summary-sig :link, .summary-sig :visited { border: none; }
+.summary-name { font-family: monospace; font-weight: bold; }
+
+/* Details tables */
+
+table.details { margin: 2em 0 0; }
+div table.details { margin-top: 0; }
+table.details tr.table-header { background: transparent; }
+table.details td.table-header { border-bottom: 1px solid #ccc; padding: 2em 0 0; }
+table.details span.table-header {
+  font: bold 140% Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif;
+  letter-spacing: -0.018em;
+}
+table.details th, table.details td { border: none; }
+table.details th th, table.details td td { border: none; }
+table.details td { padding-left: 2em; }
+table.details td td { padding-left: 0; }
+table.details h3.epydoc { margin-left: -2em; }
+table.details h3.epydoc .sig { color: #999; font-family: monospace; }
+table.details h3.epydoc .sig-name { color: #000; }
+table.details h3.epydoc .sig-arg { color: #666; }
+table.details h3.epydoc .sig-default { font-size: 95%; font-weight: normal; }
+table.details h3.epydoc .sig-default code { font-weight: normal; }
+table.details h3.epydoc .fname { color: #999; font-size: 90%;
+  font-style: italic; font-weight: normal; line-height: 1.6em;
+}
+
+dl dt { color: #666; margin-top: 1em; }
+dl dd { margin: 0; padding-left: 2em; }
+dl.fields { margin: 1em 0; padding: 0; }
+dl.fields dt { color: #666; margin-top: 1em; }
+dl.fields dd ul { margin: 0; padding: 0; }
+div.fields { font-size: 90%; margin: 0 0 2em 2em; }
+div.fields p { margin-bottom: 0.5em; }
+
+table td.footer { color: #999; font-size: 85%; margin-top: 3em;
+  padding: 0 3em 1em; position: absolute; width: 80%; }
+table td.footer :link, table td.footer :visited { border: none; color: #999; }
+table td.footer :link:hover, table td.footer :visited:hover {
+  background: transparent; text-decoration: underline;
+}
+
+/* Syntax highlighting */
+
+.py-prompt, .py-more, .variable-ellipsis, .variable-op { color: #999; }
+.variable-group { color: #666; font-weight: bold; }
+.py-string, .variable-string, .variable-quote { color: #093; }
+.py-comment { color: #06f; font-style: italic; }
+.py-keyword { color: #00f; }
+.py-output { background: #f6f6f0; color: #666; font-weight: bold; }
+
+/* Index */
+
+table.link-index { background: #f6f6f0; border: none; margin-top: 1em; }
+table.link-index td.link-index { border: none; font-family: monospace;
+  font-weight: bold; padding: .5em 1em;
+}
+table.link-index td table, table.link-index td td { border: none; }
+table.link-index .index-where { color: #999;
+  font-family: Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif;
+  font-size: 90%; font-weight: normal; line-height: 1.6em;
+}
+table.link-index .index-where :link, table.link-index .index-where :visited {
+  border: none; color: #666;
+}
+h2.epydoc { color: #999; font-size: 200%; line-height: 10px; }
new file mode 100644
--- /dev/null
+++ b/doc/templates.txt
@@ -0,0 +1,346 @@
+.. -*- mode: rst; encoding: utf-8 -*-
+
+========================
+Genshi Templating Basics
+========================
+
+Genshi provides a template engine that can be used for generating either
+markup (such as HTML_ or XML_) or plain text. While both share some of the
+syntax (and much of the underlying implementation) they are essentially
+separate languages.
+
+.. _html: http://www.w3.org/html/
+.. _xml: http://www.w3.org/XML/
+
+This document describes the common parts of the template engine and will be most
+useful as reference to those developing Genshi templates. Templates are XML or
+plain text files that include processing directives_ that affect how the
+template is rendered, and template expressions_ that are dynamically substituted
+by variable data.
+
+
+.. contents:: Contents
+   :depth: 3
+.. sectnum::
+
+--------
+Synopsis
+--------
+
+A Genshi *markup template* is a well-formed XML document with embedded Python
+used for control flow and variable substitution. Markup templates should be
+used to generate any kind of HTML or XML output, as they provide many advantages
+over simple text-based templates (such as automatic escaping of strings).
+
+The following illustrates a very basic Genshi markup template::
+
+  <?python
+    title = "A Genshi Template"
+    fruits = ["apple", "orange", "kiwi"]
+  ?>
+  <html xmlns:py="http://genshi.edgewall.org/">
+    <head>
+      <title py:content="title">This is replaced.</title>
+    </head>
+
+    <body>
+      <p>These are some of my favorite fruits:</p>
+      <ul>
+        <li py:for="fruit in fruits">
+          I like ${fruit}s
+        </li>
+      </ul>
+    </body>
+  </html>
+
+This example shows:
+
+(a) a Python code block, using a processing instruction
+(b) the Genshi namespace declaration
+(c) usage of templates directives (``py:content`` and ``py:for``)
+(d) an inline Python expression (``${fruit}``).
+
+The template would generate output similar to this::
+
+  <html>
+    <head>
+      <title>A Genshi Template</title>
+    </head>
+
+    <body>
+      <p>These are some of my favorite fruits:</p>
+      <ul>
+        <li>I like apples</li>
+        <li>I like oranges</li>
+        <li>I like kiwis</li>
+      </ul>
+    </body>
+  </html>
+
+A *text template* is a simple plain text document that can also contain embedded
+Python code. Text templates can be used to generate simple *non-markup* text
+formats, such as the body of an plain text email. For example::
+
+  Dear $name,
+  
+  These are some of my favorite fruits:
+  #for fruit in fruits
+   * $fruit
+  #end
+
+
+----------
+Python API
+----------
+
+The Python code required for templating with Genshi is generally based on the
+following pattern:
+
+* Attain a ``MarkupTemplate`` or ``TextTemplate`` object from a string or
+  file-like object containing the template source. This can either be done
+  directly, or through a ``TemplateLoader`` instance.
+* Call the ``generate()`` method of the template, passing any data that should
+  be made available to the template as keyword arguments.
+* Serialize the resulting stream using its ``render()`` method.
+
+For example::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<h1>Hello, $name!</h1>')
+  >>> stream = tmpl.generate(name='world')
+  >>> print stream.render()
+  <h1>Hello, world!</h1>
+
+Using a text template is similar::
+
+  >>> from genshi.template import TextTemplate
+  >>> tmpl = TextTemplate('Hello, $name!')
+  >>> stream = tmpl.generate(name='world')
+  >>> print stream.render()
+  Hello, world!
+
+.. note:: See the Serialization_ section of the `Markup Streams`_ page for
+          information on configuring template output options.
+
+.. _serialization: streams.html#serialization
+.. _`Markup Streams`: streams.html
+
+Using a template loader provides the advantage that ā€œcompiledā€ templates are
+automatically cached, and only parsed again when the template file changes. In
+addition, it enables the use of a *template search path*, allowing template
+directories to be spread across different file-system locations. Using a
+template loader would generally look as follows::
+
+  from genshi.template import TemplateLoader
+  loader = TemplateLoader([templates_dir1, templates_dir2])
+  tmpl = loader.load('test.html')
+  stream = tmpl.generate(title='Hello, world!')
+  print stream.render()
+
+See the `API documentation <api/index.html>`_ for details on using Genshi via
+the Python API.
+
+
+.. _`expressions`:
+
+------------------------------------
+Template Expressions and Code Blocks
+------------------------------------
+
+Python_ expressions can be used in text and directive arguments. An expression
+is substituted with the result of its evaluation against the template data.
+Expressions in text (which includes the values of non-directive attributes) need
+to prefixed with a dollar sign (``$``) and usually enclosed in curly braces
+(``{ā€¦}``).
+
+.. _python: http://www.python.org/
+
+If the expression starts with a letter and contains only letters, digits, dots,
+and underscores, the curly braces may be omitted. In all other cases, the
+braces are required so that the template processor knows where the expression
+ends::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<em>${items[0].capitalize()} item</em>')
+  >>> print tmpl.generate(items=['first', 'second'])
+  <em>First item</em>
+
+Expressions support the full power of Python. In addition, it is possible to
+access items in a dictionary using ā€œdotted notationā€ (i.e. as if they were
+attributes), and vice-versa (i.e. access attributes as if they were items in a
+dictionary)::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<em>${dict.foo}</em>')
+  >>> print tmpl.generate(dict={'foo': 'bar'})
+  <em>bar</em>
+
+Because there are two ways to access either attributes or items, expressions
+do not raise the standard ``AttributeError`` or ``IndexError`` exceptions, but
+rather an exception of the type ``UndefinedError``. The same kind of error is
+raised when you try to use a top-level variable that is not in the context data.
+See `Error Handling`_ below for details on how such errors are handled.
+
+
+.. _`code blocks`:
+
+Code Blocks
+===========
+
+XML templates also support full Python code blocks using the ``<?python ?>``
+processing instruction::
+
+  <div>
+    <?python
+        from genshi.builder import tag
+        def greeting(name):
+            return tag.b('Hello, %s!' % name') ?>
+    ${greeting('world')}
+  </div>
+
+This will produce the following output::
+
+  <div>
+    <b>Hello, world!</b>
+  </div>
+
+Code blocks can import modules, define classes and functions, and basically do
+anything you can do in normal Python code. What code blocks can *not* do is to
+produce content that is included directly in the generated page.
+
+.. note:: Using the ``print`` statement will print to the standard output
+          stream, just as it does for other Python code in your application.
+
+Unlike expressions, Python code in ``<?python ?>`` processing instructions can
+not use item and attribute access in an interchangeable manner. That means that
+ā€œdotted notationā€ is always attribute access, and vice-versa.
+
+The support for Python code blocks in templates is not supposed to encourage
+mixing application code into templates, which is generally considered bad
+design. If you're using many code blocks, that may be a sign that you should
+move such code into separate Python modules.
+
+.. note:: Code blocks are not currently supported in text templates.
+
+
+.. _`error handling`:
+
+Error Handling
+==============
+
+By default, Genshi allows you to access variables that are not defined, without
+raising a ``NameError`` exception as regular Python code would::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<p>${doh}</p>')
+  >>> print tmpl.generate().render('xhtml')
+  <p></p>
+
+You *will* however get an exception if you try to call an undefined variable, or
+do anything else with it, such as accessing its attributes::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<p>${doh.oops}</p>')
+  >>> print tmpl.generate().render('xhtml')
+  Traceback (most recent call last):
+    ...
+  UndefinedError: "doh" not defined
+
+If you need to know whether a variable is defined, you can check its type
+against the ``Undefined`` class, for example in a conditional directive::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<p>${type(doh) is not Undefined}</p>')
+  >>> print tmpl.generate().render('xhtml')
+  <p>False</p>
+
+Alternatively, the built-in functions defined_ or value_of_ can be used in this
+case.
+
+Strict Mode
+-----------
+
+In addition to the default "lenient" error handling, Genshi lets you use a less
+forgiving mode if you prefer errors blowing up loudly instead of being ignored
+silently.
+
+This mode can be chosen by passing the ``lookup='strict'`` keyword argument to
+the template initializer, or by passing the ``variable_lookup='strict'`` keyword
+argument to the ``TemplateLoader`` initializer::
+
+  >>> from genshi.template import MarkupTemplate
+  >>> tmpl = MarkupTemplate('<p>${doh}</p>', lookup='strict')
+  >>> print tmpl.generate().render('xhtml')
+  Traceback (most recent call last):
+    ...
+  UndefinedError: "doh" not defined
+
+When using strict mode, any reference to an undefined variable, as well as
+trying to access an non-existing item or attribute of an object, will cause an
+``UndefinedError`` to be raised immediately.
+
+.. note:: While this mode is currently not the default, it may be promoted to
+          the default in future versions of Genshi. In general, the default
+          lenient error handling mode can be considered dangerous as it silently
+          ignores typos.
+
+Custom Modes
+------------
+
+In addition to the built-in "lenient" and "strict" modes, it is also possible to
+use a custom error handling mode. For example, you could use lenient error
+handling in a production environment, while also logging a warning when an
+undefined variable is referenced.
+
+See the API documentation of the ``genshi.template.eval`` module for details.
+
+
+Built-in Functions & Types
+==========================
+
+The following functions and types are available by default in template code, in
+addition to the standard built-ins that are available to all Python code.
+
+.. _`defined`:
+
+``defined(name)``
+-----------------
+This function determines whether a variable of the specified name exists in
+the context data, and returns ``True`` if it does.
+ 
+.. _`value_of`:
+
+``value_of(name, default=None)``
+--------------------------------
+This function returns the value of the variable with the specified name if
+such a variable is defined, and returns the value of the ``default``
+parameter if no such variable is defined.
+
+.. _`Markup`:
+
+``Markup(text)``
+----------------
+The ``Markup`` type marks a given string as being safe for inclusion in markup,
+meaning it will *not* be escaped in the serialization stage. Use this with care,
+as not escaping a user-provided string may allow malicious users to open your
+web site to cross-site scripting attacks.
+
+.. _`Undefined`:
+
+``Undefined``
+----------------
+The ``Undefined`` type can be used to check whether a reference variable is
+defined, as explained in `error handling`_.
+
+
+.. _`directives`:
+
+-------------------
+Template Directives
+-------------------
+
+Directives provide control flow functionality for templates, such as conditions
+or iteration. As the syntax for directives depends on whether you're using
+markup or text templates, refer to the
+`XML Template Language <xml-templates.html>`_ or
+`Text Template Language <text-templates.html>`_ pages for information.
--- a/doc/text-templates.txt
+++ b/doc/text-templates.txt
@@ -12,104 +12,19 @@
 .. _velocity: http://jakarta.apache.org/velocity/
 
 This document describes the template language and will be most useful as
-reference to those developing Genshi text templates. Templates are XML files of some
-kind (such as XHTML) that include processing directives_ (elements or
-attributes identified by a separate namespace) that affect how the template is
-rendered, and template expressions_ that are dynamically substituted by
+reference to those developing Genshi text templates. Templates are text files of
+some kind that include processing directives_ that affect how the template is
+rendered, and template expressions that are dynamically substituted by
 variable data.
 
+See `Genshi Templating Basics <templates.html>`_ for general information on
+embedding Python code in templates.
+
 
 .. contents:: Contents
    :depth: 3
 .. sectnum::
 
-----------
-Python API
-----------
-
-The Python code required for templating with Genshi is generally based on the
-following pattern:
-
-* Attain a ``TextTemplate`` object from a string or file object containing the
-  template source. This can either be done directly, or through a
-  ``TemplateLoader`` instance.
-* Call the ``generate()`` method of the template, passing any data that should
-  be made available to the template as keyword arguments.
-* Serialize the resulting stream using its ``render()`` method.
-
-For example::
-
-  from genshi.template import TextTemplate
-
-  tmpl = TextTemplate('$title')
-  stream = tmpl.generate(title='Hello, world!')
-  print stream.render('text')
-
-That code would produce the following output::
-
-  Hello, world!
-
-Using a template loader provides the advantage that ā€œcompiledā€ templates are
-automatically cached, and only parsed again when the template file changes::
-
-  from genshi.template import TemplateLoader
-
-  loader = TemplateLoader([templates_dir])
-  tmpl = loader.load('test.txt' cls=TextTemplate)
-  stream = tmpl.generate(title='Hello, world!')
-  print stream.render('text')
-
-
-.. _`expressions`:
-
---------------------
-Template Expressions
---------------------
-
-Python_ expressions can be used in text and as arguments to directives_. An expression is substituted with the result of its evaluation against the
-template data. Expressions need to prefixed with a dollar sign (``$``) and 
-usually enclosed in curly braces (``{ā€¦}``).
-
-.. _python: http://www.python.org/
-
-If the expression starts with a letter and contains only letters, digits, dots,
-and underscores, the curly braces may be omitted. In all other cases, the
-braces are required so that the template processor knows where the expression
-ends::
-
-  >>> from genshi.template import TextTemplate
-  >>> tmpl = TextTemplate('${items[0].capitalize()} item')
-  >>> print tmpl.generate(items=['first', 'second'])
-  First item
-
-Expressions support the full power of Python. In addition, it is possible to
-access items in a dictionary using ā€œdotted notationā€ (i.e. as if they were
-attributes), and vice-versa (i.e. access attributes as if they were items in
-a dictionary)::
-
-  >>> from genshi.template import TextTemplate
-  >>> tmpl = TextTemplate('${dict.foo}')
-  >>> print tmpl.generate(dict={'foo': 'bar'})
-  bar
-
-Another difference is that you can access variables that are not defined, and
-won't get a ``NameError`` exception::
-
-  >>> from genshi.template import TextTemplate
-  >>> tmpl = TextTemplate('${doh}')
-  >>> print tmpl.generate()
-  <BLANKLINE>
-
-You **will** however get a ``NameError`` if you try to call an undefined 
-variable, or do anything else with it, such as accessing its attributes. If you
-need to know whether a variable is defined, you can check its type against the
-``Undefined`` class, for example in an `#if`_ directive::
-
-  >>> from genshi.template import TextTemplate
-  >>> tmpl = TextTemplate('${type(doh) is Undefined}')
-  >>> print tmpl.generate()
-  True
-
 
 .. _`directives`:
 
@@ -269,6 +184,40 @@
     Hello, world!
 
 
+.. _includes:
+.. _`#include`:
+
+``#include``
+------------
+
+To reuse common parts of template text across template files, you can include
+other files using the ``#include`` directive::
+
+  #include "base.txt"
+
+Any content included this way is inserted into the generated output. The
+included template sees the context data as it exists at the point of the
+include. `Macros`_ in the included template are also available to the including
+template after the point it was included.
+
+Include paths are relative to the filename of the template currently being
+processed. So if the example above was in the file "``myapp/mail.txt``"
+(relative to the template search path), the include directive would look for
+the included file at "``myapp/base.txt``". You can also use Unix-style
+relative paths, for example "``../base.txt``" to look in the parent directory.
+
+Just like other directives, the argument to the ``#include`` directive accepts
+any Python expression, so the path to the included template can be determined
+dynamically::
+
+  #include '%s.txt' % filename
+
+Note that a ``TemplateNotFound`` exception is raised if an included file can't
+be found.
+
+.. note:: The include directive for text templates was added in Genshi 0.5.
+
+
 Variable Binding
 ================
 
--- a/doc/xml-templates.txt
+++ b/doc/xml-templates.txt
@@ -18,100 +18,17 @@
 reference to those developing Genshi XML templates. Templates are XML files of
 some kind (such as XHTML) that include processing directives_ (elements or
 attributes identified by a separate namespace) that affect how the template is
-rendered, and template expressions_ that are dynamically substituted by
+rendered, and template expressions that are dynamically substituted by
 variable data.
 
+See `Genshi Templating Basics <templates.html>`_ for general information on
+embedding Python code in templates.
+
 
 .. contents:: Contents
    :depth: 3
 .. sectnum::
 
-----------
-Python API
-----------
-
-The Python code required for templating with Genshi is generally based on the
-following pattern:
-
-* Attain a ``MarkupTemplate`` object from a string or file object containing
-  the template XML source. This can either be done directly, or through a
-  ``TemplateLoader`` instance.
-* Call the ``generate()`` method of the template, passing any data that should
-  be made available to the template as keyword arguments.
-* Serialize the resulting stream using its ``render()`` method.
-
-For example::
-
-  from genshi.template import MarkupTemplate
-
-  tmpl = MarkupTemplate('<h1>$title</h1>')
-  stream = tmpl.generate(title='Hello, world!')
-  print stream.render('xhtml')
-
-That code would produce the following output::
-
-  <h1>Hello, world!</h1>
-
-However, if you want includes_ to work, you should attain the template instance
-through a ``TemplateLoader``, and load the template from a file::
-
-  from genshi.template import TemplateLoader
-
-  loader = TemplateLoader([templates_dir])
-  tmpl = loader.load('test.html')
-  stream = tmpl.generate(title='Hello, world!')
-  print stream.render('xhtml')
-
-
-.. _`expressions`:
-
---------------------
-Template Expressions
---------------------
-
-Python_ expressions can be used in text and attribute values. An expression is
-substituted with the result of its evaluation against the template data.
-Expressions need to prefixed with a dollar sign (``$``) and usually enclosed in
-curly braces (``{ā€¦}``).
-
-If the expression starts with a letter and contains only letters, digits, dots,
-and underscores, the curly braces may be omitted. In all other cases, the
-braces are required so that the template processor knows where the expression
-ends::
-
-  >>> from genshi.template import MarkupTemplate
-  >>> tmpl = MarkupTemplate('<em>${items[0].capitalize()} item</em>')
-  >>> print tmpl.generate(items=['first', 'second'])
-  <em>First item</em>
-
-Expressions support the full power of Python. In addition, it is possible to
-access items in a dictionary using ā€œdotted notationā€ (i.e. as if they were
-attributes), and vice-versa (i.e. access attributes as if they were items in a
-dictionary)::
-
-  >>> from genshi.template import MarkupTemplate
-  >>> tmpl = MarkupTemplate('<em>${dict.foo}</em>')
-  >>> print tmpl.generate(dict={'foo': 'bar'})
-  <em>bar</em>
-
-Another difference is that you can access variables that are not defined, and
-won't get a ``NameError`` exception::
-
-  >>> from genshi.template import TextTemplate
-  >>> tmpl = TextTemplate('${doh}')
-  >>> print tmpl.generate()
-  <BLANKLINE>
-
-You **will** however get a ``NameError`` if you try to call an undefined 
-variable, or do anything else with it, such as accessing its attributes. If you
-need to know whether a variable is defined, you can check its type against the
-``Undefined`` class, for example in a `py:if`_ directive::
-
-  >>> from genshi.template import TextTemplate
-  >>> tmpl = TextTemplate('${type(doh) is Undefined}')
-  >>> print tmpl.generate()
-  True
-
 
 .. _`directives`:
 
@@ -207,9 +124,9 @@
 -------------
 
 The ``py:choose`` directive, in combination with the directives ``py:when``
-and ``py:otherwise`` provides advanced contional processing for rendering one
+and ``py:otherwise`` provides advanced conditional processing for rendering one
 of several alternatives. The first matching ``py:when`` branch is rendered, or,
-if no ``py:when`` branch matches, the ``py:otherwise`` branch is be rendered.
+if no ``py:when`` branch matches, the ``py:otherwise`` branch is rendered.
 
 If the ``py:choose`` directive is empty the nested ``py:when`` directives will
 be tested for truth::
@@ -360,8 +277,10 @@
 
 Inside the body of a ``py:match`` directive, the ``select(path)`` function is
 made available so that parts or all of the original element can be incorporated
-in the output of the match template. See [wiki:GenshiStream#UsingXPath] for
-more information about this function.
+in the output of the match template. See `Using XPath`_ for more information
+about this function.
+
+.. _`Using XPath`: streams.html#using-xpath
 
 This directive can also be used as an element::
 
@@ -576,7 +495,7 @@
 .. _`xinclude specification`: http://www.w3.org/TR/xinclude/
 
 Incudes in Genshi are fully dynamic: Just like normal attributes, the `href`
-attribute accepts expressions_, and directives_ can be used on the
+attribute accepts expressions, and directives_ can be used on the
 ``<xi:include />`` element just as on any other element, meaning you can do
 things like conditional includes::
 
--- a/examples/bench/basic.py
+++ b/examples/bench/basic.py
@@ -1,3 +1,8 @@
+# -*- encoding: utf-8 -*-
+# Template language benchmarks
+#
+# Objective: Test general templating features using a small template
+
 from cgi import escape
 import os
 from StringIO import StringIO
@@ -20,7 +25,11 @@
     return render
 
 def myghty(dirname, verbose=False):
-    from myghty import interp
+    try:
+        from myghty import interp
+    except ImportError:
+        print>>sys.stderr, 'Mighty not installed, skipping'
+        return lambda: None
     interpreter = interp.Interpreter(component_root=dirname)
     def render():
         data = dict(title='Just a test', user='joe',
@@ -34,7 +43,11 @@
 
 def cheetah(dirname, verbose=False):
     # FIXME: infinite recursion somewhere... WTF?
-    from Cheetah.Template import Template
+    try:
+        from Cheetah.Template import Template
+    except ImportError:
+        print>>sys.stderr, 'Cheetah not installed, skipping'
+        return lambda: None
     class MyTemplate(Template):
         def serverSidePath(self, path): return os.path.join(dirname, path)
     filename = os.path.join(dirname, 'template.tmpl')
@@ -54,7 +67,8 @@
     try:
         import neo_cgi
     except ImportError:
-        return lambda:None
+        print>>sys.stderr, 'ClearSilver not installed, skipping'
+        return lambda: None
     neo_cgi.update()
     import neo_util
     import neo_cs
@@ -74,8 +88,12 @@
     return render
 
 def django(dirname, verbose=False):
-    from django.conf import settings
-    settings.configure(TEMPLATE_DIRS=[os.path.join(dirname, 'templates')])
+    try:
+        from django.conf import settings
+        settings.configure(TEMPLATE_DIRS=[os.path.join(dirname, 'templates')])
+    except ImportError:
+        print>>sys.stderr, 'Django not installed, skipping'
+        return lambda: None
     from django import template, templatetags
     from django.template import loader
     templatetags.__path__.append(os.path.join(dirname, 'templatetags'))
@@ -91,32 +109,29 @@
     return render
 
 def kid(dirname, verbose=False):
-    import kid
+    try:
+        import kid
+    except ImportError:
+        print>>sys.stderr, "SimpleTAL not installed, skipping"
+        return lambda: None
     kid.path = kid.TemplatePath([dirname])
-    template = kid.Template(file='template.kid')
+    template = kid.load_template('template.kid').Template
     def render():
-        template = kid.Template(file='template.kid',
-                                title='Just a test', user='joe',
-                                items=['Number %d' % num for num in range(1, 15)])
-        return template.serialize(output='xhtml')
-
-    if verbose:
-        print render()
-    return render
-
-def nevow(dirname, verbose=False):
-    # FIXME: can't figure out the API
-    from nevow.loaders import xmlfile
-    template = xmlfile('template.xml', templateDir=dirname).load()
-    def render():
-        print template
+        return template(
+            title='Just a test', user='joe',
+            items=['Number %d' % num for num in range(1, 15)]
+        ).serialize(output='xhtml')
 
     if verbose:
         print render()
     return render
 
 def simpletal(dirname, verbose=False):
-    from simpletal import simpleTAL, simpleTALES
+    try:
+        from simpletal import simpleTAL, simpleTALES
+    except ImportError:
+        print>>sys.stderr, "SimpleTAL not installed, skipping"
+        return lambda: None
     fileobj = open(os.path.join(dirname, 'base.html'))
     base = simpleTAL.compileHTMLTemplate(fileobj)
     fileobj.close()
--- a/examples/bench/bigtable.py
+++ b/examples/bench/bigtable.py
@@ -134,7 +134,6 @@
         kid_tmpl.table = table
         kid_tmpl.serialize(output='html')
 
-
     if cet:
         def test_kid_et():
             """Kid template + cElementTree"""
@@ -186,9 +185,9 @@
 
 
 def run(which=None, number=10):
-    tests = ['test_builder', 'test_genshi', 'test_genshi_builder', 'test_myghty', 'test_kid',
-             'test_kid_et', 'test_et', 'test_cet', 'test_clearsilver',
-             'test_django']
+    tests = ['test_builder', 'test_genshi', 'test_genshi_builder',
+             'test_myghty', 'test_kid', 'test_kid_et', 'test_et', 'test_cet',
+             'test_clearsilver', 'test_django']
 
     if which:
         tests = filter(lambda n: n[5:] in which, tests)
--- a/examples/bench/django/templates/base.html
+++ b/examples/bench/django/templates/base.html
@@ -3,12 +3,16 @@
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
 
-  {% block body %}
-    <div id="header">
-      <h1>{{ title|escape }}</h1>
-    </div>
-    {{ block.super }}
-    <div id="footer"></div>
+  {% block content %}
+    {% block header %}
+      <div id="header">
+        <h1>{{ title|escape }}</h1>
+      </div>
+    {% endblock %}
+
+    {% block footer %}
+      <div id="footer"></div>
+    {% endblock %}
   {% endblock %}
 
 </html>
--- a/examples/bench/django/templates/template.html
+++ b/examples/bench/django/templates/template.html
@@ -1,22 +1,27 @@
 {% extends "base.html" %}
 {% load bench %}
 
-<head>
-  <title>${title|escape}</title>
-</head>
-
-{% block body %}
-  <div>{% greeting user %}</div>
-  <div>{% greeting "me" %}</div>
-  <div>{% greeting "world" %}</div>
+{% block content %}
+  <head>
+    <title>{{title|escape}}</title>
+  </head>
 
-  <h2>Loop</h2>
-  {% if items %}
-    <ul>
-      {% for item in items %}
-        <li{% if forloop.islast %} class="last"{% endif %}>{{ item|escape }}</li>
-      {% endfor %}
-    </ul>
-  {% endif %}
+  <body>
+    {% block header %}{% endblock %}
 
+    <div>{% greeting user %}</div>
+    <div>{% greeting "me" %}</div>
+    <div>{% greeting "world" %}</div>
+
+    <h2>Loop</h2>
+    {% if items %}
+      <ul>
+        {% for item in items %}
+          <li{% if forloop.last %} class="last"{% endif %}>{{ item|escape }}</li>
+        {% endfor %}
+      </ul>
+    {% endif %}
+
+    {% block footer %}{% endblock %}
+  </body>
 {% endblock %}
--- a/examples/cherrypy/index.py
+++ b/examples/cherrypy/index.py
@@ -15,8 +15,5 @@
         return tmpl.generate(name='world').render('xhtml')
 
 
-cherrypy.root = Example()
-
 if __name__ == '__main__':
-    cherrypy.config.update(file='config.txt')
-    cherrypy.server.start()
+    cherrypy.quickstart(Example(), config='config.txt')
--- a/examples/turbogears/genshitest/controllers.py
+++ b/examples/turbogears/genshitest/controllers.py
@@ -18,9 +18,7 @@
         log.debug("Happy TurboGears Controller Responding For Duty")
         return dict(now=time.ctime(),
                     widget=widgets.TextArea(name="widget_test",
-                                            default="This is a test of using "
-                                                    "TurboGears widgets with "
-                                                    "Genshi",
+                                            default="Lorem ipsum",
                                             rows=5, cols=40))
 
     @expose(template="genshi-text:genshitest.templates.plain",
--- a/examples/turbogears/genshitest/templates/master.html
+++ b/examples/turbogears/genshitest/templates/master.html
@@ -1,43 +1,36 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
       xmlns:py="http://genshi.edgewall.org/"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
       py:strip="">
-
-<xi:include href="sitetemplate.html"><xi:fallback/></xi:include>
+  <xi:include href="sitetemplate.html"><xi:fallback/></xi:include>
 
-<head py:match="head" py:attrs="select('@*')">
-    <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+  <head py:match="head" py:attrs="select('@*')">
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type"
+          py:replace="''" />
     <title py:replace="''">Your title goes here</title>
-    <meta py:replace="select('*')"/>
+    <meta py:replace="select('*')" />
     <style type="text/css">
-        #pageLogin
-        {
-            font-size: 10px;
-            font-family: verdana;
-            text-align: right;
-        }
+      #pageLogin { font-size: 10px; font-family: verdana; text-align: right; }
     </style>
-</head>
+  </head>
 
-<body py:match="body" py:attrs="select('@*')">
-    <div py:if="tg.config('identity.on',False) and not logging_in"
-        id="pageLogin"
-        py:choose="">
-        <span py:when="tg.identity.anonymous">
-            <a href="/login">Login</a>
-        </span>
-        <span py:otherwise="">
-            Welcome ${tg.identity.user.display_name}.
-            <a href="/logout">Logout</a>
-        </span>
+  <body py:match="body" py:attrs="select('@*')">
+    <div id="pageLogin"
+         py:if="tg.config('identity.on', False) and not value_of('logging_in')"
+         py:choose="">
+      <span py:when="tg.identity.anonymous">
+        <a href="/login">Login</a>
+      </span>
+      <span py:otherwise="">
+        Welcome ${tg.identity.user.display_name}.
+        <a href="/logout">Logout</a>
+      </span>
     </div>
-
     <div py:if="tg_flash" class="flash" py:content="tg_flash"></div>
-
-    <div py:replace="select('*|text()')"/>
-
+    <div py:replace="select('*|text()')" />
     <p align="center"><img src="/static/images/tg_under_the_hood.png" alt="TurboGears under the hood"/></p>
-</body>
+  </body>
 
 </html>
--- a/examples/turbogears/genshitest/templates/welcome.html
+++ b/examples/turbogears/genshitest/templates/welcome.html
@@ -1,39 +1,34 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:py="http://genshi.edgewall.org/"
-      xmlns:xi="http://www.w3.org/2001/XInclude">
-<xi:include href="master.html" />
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+  <xi:include href="master.html" />
 
-<head>
+  <head>
     <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
     <title>Welcome to TurboGears</title>
-</head>
-
-<body>
-    <p>Congratulations, your TurboGears application is running as of <span py:replace="now">now</span>.</p>
-
-    <h2>Are you ready to Gear Up?</h2>
-
-    <p>Take the following steps to dive right in:</p>
+  </head>
 
-    <ol>
-        <li>Edit your project's model.py to create SQLObjects representing the data you're working with</li>
-        <li>Edit your dev.cfg file to point to the database you'll be using</li>
-        <li>Run "<code>tg-admin sql create</code>" to create the tables in the database</li>
-        <li>Edit controllers.py to add the functionality to your webapp</li>
-        <li>Change the master.kid template to have the headers and footers for your application.</li>
-        <li>Change welcome.kid (this template) or create a new one to display your data</li>
-        <li>Repeat steps 4-6 until done.</li>
-        <li><b>Profit!</b></li>
-    </ol>
+  <body>
+    <p>Congratulations, your TurboGears application is running as of
+    <span py:replace="now">now</span>.</p>
 
-    <p>If you haven't already, you might check out some of the <a href="http://www.turbogears.org/docs/" >documentation</a>.</p>
+    <h2>Using Genshi in TurboGears</h2>
 
-    <p>Thanks for using TurboGears! See you on the <a href="http://groups.google.com/group/turbogears" >mailing list</a> and the "turbogears" channel on irc.freenode.org!</p>
+    <p>
+      Please see the online
+      <a href="http://genshi.edgewall.org/wiki/Documentation">documentation</a>
+      for general information on Genshi.
+    </p>
 
+    <p>Here's an example for using a TurboGears widget in a Genshi template:</p>
     ${ET(widget.display())}
 
-    <p>Here's a <a href="plain">link</a> to the output of a plain-text template.</p>
+    <p>
+      And here's a <a href="plain">link</a> to the output of a plain-text
+      template.
+    </p>
 
-</body>
+  </body>
 </html>
--- a/genshi/__init__.py
+++ b/genshi/__init__.py
@@ -19,5 +19,11 @@
 independently of where or how they are produced.
 """
 
+__docformat__ = 'restructuredtext en'
+try:
+    __version__ = __import__('pkg_resources').get_distribution('Genshi').version
+except ImportError:
+    pass
+
 from genshi.core import *
 from genshi.input import ParseError, XML, HTML
--- a/genshi/builder.py
+++ b/genshi/builder.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -11,9 +11,67 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://genshi.edgewall.org/log/.
 
+"""Support for programmatically generating markup streams from Python code using
+a very simple syntax. The main entry point to this module is the `tag` object
+(which is actually an instance of the ``ElementFactory`` class). You should
+rarely (if ever) need to directly import and use any of the other classes in
+this module.
+
+Elements can be created using the `tag` object using attribute access. For
+example:
+
+>>> doc = tag.p('Some text and ', tag.a('a link', href='http://example.org/'), '.')
+>>> doc
+<Element "p">
+
+This produces an `Element` instance which can be further modified to add child
+nodes and attributes. This is done by "calling" the element: positional
+arguments are added as child nodes (alternatively, the `Element.append` method
+can be used for that purpose), whereas keywords arguments are added as
+attributes:
+
+>>> doc(tag.br)
+<Element "p">
+>>> print doc
+<p>Some text and <a href="http://example.org/">a link</a>.<br/></p>
+
+If an attribute name collides with a Python keyword, simply append an underscore
+to the name:
+
+>>> doc(class_='intro')
+<Element "p">
+>>> print doc
+<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
+
+As shown above, an `Element` can easily be directly rendered to XML text by
+printing it or using the Python ``str()`` function. This is basically a
+shortcut for converting the `Element` to a stream and serializing that
+stream:
+
+>>> stream = doc.generate()
+>>> stream #doctest: +ELLIPSIS
+<genshi.core.Stream object at ...>
+>>> print stream
+<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
+
+
+The `tag` object also allows creating "fragments", which are basically lists
+of nodes (elements or text) that don't have a parent element. This can be useful
+for creating snippets of markup that are attached to a parent element later (for
+example in a template). Fragments are created by calling the `tag` object, which
+returns an object of type `Fragment`:
+
+>>> fragment = tag('Hello, ', tag.em('world'), '!')
+>>> fragment
+<Fragment>
+>>> print fragment
+Hello, <em>world</em>!
+"""
+
 from genshi.core import Attrs, Namespace, QName, Stream, START, END, TEXT
 
-__all__ = ['Fragment', 'Element', 'tag']
+__all__ = ['Fragment', 'Element', 'ElementFactory', 'tag']
+__docformat__ = 'restructuredtext en'
 
 
 class Fragment(object):
@@ -23,12 +81,17 @@
     __slots__ = ['children']
 
     def __init__(self):
+        """Create a new fragment."""
         self.children = []
 
     def __add__(self, other):
         return Fragment()(self, other)
 
     def __call__(self, *args):
+        """Append any positional arguments as child nodes.
+        
+        :see: `append`
+        """
         map(self.append, args)
         return self
 
@@ -45,7 +108,11 @@
         return unicode(self.generate())
 
     def append(self, node):
-        """Append an element or string as child node."""
+        """Append an element or string as child node.
+        
+        :param node: the node to append; can be an `Element`, `Fragment`, or a
+                     `Stream`, or a Python string or number
+        """
         if isinstance(node, (Stream, Element, basestring, int, float, long)):
             # For objects of a known/primitive type, we avoid the check for
             # whether it is iterable for better performance
@@ -72,7 +139,10 @@
                 yield TEXT, child, (None, -1, -1)
 
     def generate(self):
-        """Return a markup event stream for the fragment."""
+        """Return a markup event stream for the fragment.
+        
+        :rtype: `Stream`
+        """
         return Stream(self._generate())
 
 
@@ -82,7 +152,7 @@
     return unicode(value)
 
 def _kwargs_to_attrs(kwargs):
-    return [(k.rstrip('_').replace('_', '-'), _value_to_unicode(v))
+    return [(QName(k.rstrip('_').replace('_', '-')), _value_to_unicode(v))
             for k, v in kwargs.items() if v is not None]
 
 
@@ -163,7 +233,7 @@
     >>> from genshi.core import Namespace
     >>> xhtml = Namespace('http://www.w3.org/1999/xhtml')
     >>> print Element(xhtml.html, lang='en')
-    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"/>
+    <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
     """
     __slots__ = ['tag', 'attrib']
 
@@ -173,6 +243,13 @@
         self.attrib = Attrs(_kwargs_to_attrs(attrib))
 
     def __call__(self, *args, **kwargs):
+        """Append any positional arguments as child nodes, and keyword arguments
+        as attributes.
+        
+        :return: the element itself so that calls can be chained
+        :rtype: `Element`
+        :see: `Fragment.append`
+        """
         self.attrib |= Attrs(_kwargs_to_attrs(kwargs))
         Fragment.__call__(self, *args)
         return self
@@ -187,7 +264,10 @@
         yield END, self.tag, (None, -1, -1)
 
     def generate(self):
-        """Return a markup event stream for the fragment."""
+        """Return a markup event stream for the fragment.
+        
+        :rtype: `Stream`
+        """
         return Stream(self._generate())
 
 
@@ -213,14 +293,14 @@
     
     >>> factory = ElementFactory('http://www.w3.org/1999/xhtml')
     >>> print factory.html(lang="en")
-    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"/>
+    <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
     
     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>
+    <html><g xmlns="http://www.w3.org/2000/svg" id="3"/></html>
     
     Usually, the `ElementFactory` class is not be used directly. Rather, the
     `tag` instance should be used to create elements.
@@ -229,23 +309,44 @@
     def __init__(self, namespace=None):
         """Create the factory, optionally bound to the given namespace.
         
-        @param namespace: the namespace URI for any created elements, or `None`
-            for no 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 __call__(self, *args):
+        """Create a fragment that has the given positional arguments as child
+        nodes.
+
+        :return: the created `Fragment`
+        :rtype: `Fragment`
+        """
         return Fragment()(*args)
 
     def __getitem__(self, namespace):
-        """Return a new factory that is bound to the specified namespace."""
+        """Return a new factory that is bound to the specified namespace.
+        
+        :param namespace: the namespace URI or `Namespace` object
+        :return: an `ElementFactory` that produces elements bound to the given
+                 namespace
+        :rtype: `ElementFactory`
+        """
         return ElementFactory(namespace)
 
     def __getattr__(self, name):
-        """Create an `Element` with the given name."""
+        """Create an `Element` with the given name.
+        
+        :param name: the tag name of the element to create
+        :return: an `Element` with the specified name
+        :rtype: `Element`
+        """
         return Element(self.namespace and self.namespace[name] or name)
 
 
 tag = ElementFactory()
+"""Global `ElementFactory` bound to the default namespace.
+
+:type: `ElementFactory`
+"""
--- a/genshi/core.py
+++ b/genshi/core.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -19,6 +19,7 @@
 
 __all__ = ['Stream', 'Markup', 'escape', 'unescape', 'Attrs', 'Namespace',
            'QName']
+__docformat__ = 'restructuredtext en'
 
 
 class StreamEventKind(str):
@@ -35,14 +36,15 @@
     
     This class is basically an iterator over the events.
     
-    Stream events are tuples of the form:
+    Stream events are tuples of the form::
     
       (kind, data, position)
     
-    where `kind` is the event kind (such as `START`, `END`, `TEXT`, etc), `data`
-    depends on the kind of event, and `position` is a `(filename, line, offset)`
-    tuple that contains the location of the original element or text in the
-    input. If the original location is unknown, `position` is `(None, -1, -1)`.
+    where ``kind`` is the event kind (such as `START`, `END`, `TEXT`, etc),
+    ``data`` depends on the kind of event, and ``position`` is a
+    ``(filename, line, offset)`` tuple that contains the location of the
+    original element or text in the input. If the original location is unknown,
+    ``position`` is ``(None, -1, -1)``.
     
     Also provided are ways to serialize the stream to text. The `serialize()`
     method will return an iterator over generated strings, while `render()`
@@ -51,23 +53,24 @@
     """
     __slots__ = ['events']
 
-    START = StreamEventKind('START') # a start tag
-    END = StreamEventKind('END') # an end tag
-    TEXT = StreamEventKind('TEXT') # literal text
-    DOCTYPE = StreamEventKind('DOCTYPE') # doctype declaration
-    START_NS = StreamEventKind('START_NS') # start namespace mapping
-    END_NS = StreamEventKind('END_NS') # end namespace mapping
-    START_CDATA = StreamEventKind('START_CDATA') # start CDATA section
-    END_CDATA = StreamEventKind('END_CDATA') # end CDATA section
-    PI = StreamEventKind('PI') # processing instruction
-    COMMENT = StreamEventKind('COMMENT') # comment
+    START = StreamEventKind('START') #: a start tag
+    END = StreamEventKind('END') #: an end tag
+    TEXT = StreamEventKind('TEXT') #: literal text
+    XML_DECL = StreamEventKind('XML_DECL') #: XML declaration
+    DOCTYPE = StreamEventKind('DOCTYPE') #: doctype declaration
+    START_NS = StreamEventKind('START_NS') #: start namespace mapping
+    END_NS = StreamEventKind('END_NS') #: end namespace mapping
+    START_CDATA = StreamEventKind('START_CDATA') #: start CDATA section
+    END_CDATA = StreamEventKind('END_CDATA') #: end CDATA section
+    PI = StreamEventKind('PI') #: processing instruction
+    COMMENT = StreamEventKind('COMMENT') #: comment
 
     def __init__(self, events):
         """Initialize the stream with a sequence of markup events.
         
-        @param events: a sequence or iterable providing the events
+        :param events: a sequence or iterable providing the events
         """
-        self.events = events
+        self.events = events #: The underlying iterable producing the events
 
     def __iter__(self):
         return iter(self.events)
@@ -111,6 +114,10 @@
         
         Commonly, serializers should be used at the end of the "pipeline";
         using them somewhere in the middle may produce unexpected results.
+        
+        :param function: the callable object that should be applied as a filter
+        :return: the filtered stream
+        :rtype: `Stream`
         """
         return Stream(_ensure(function(self)))
 
@@ -121,42 +128,51 @@
         filters must be callables that accept the stream object as parameter,
         and return the filtered stream.
         
-        The call:
+        The call::
         
             stream.filter(filter1, filter2)
         
-        is equivalent to:
+        is equivalent to::
         
             stream | filter1 | filter2
+        
+        :param filters: one or more callable objects that should be applied as
+                        filters
+        :return: the filtered stream
+        :rtype: `Stream`
         """
         return reduce(operator.or_, (self,) + filters)
 
     def render(self, method='xml', encoding='utf-8', **kwargs):
         """Return a string representation of the stream.
         
-        @param method: determines how the stream is serialized; can be either
-                       "xml", "xhtml", "html", "text", or a custom serializer
-                       class
-        @param encoding: how the output string should be encoded; if set to
-                         `None`, this method returns a `unicode` object
-
         Any additional keyword arguments are passed to the serializer, and thus
         depend on the `method` parameter value.
+        
+        :param method: determines how the stream is serialized; can be either
+                       "xml", "xhtml", "html", "text", or a custom serializer
+                       class
+        :param encoding: how the output string should be encoded; if set to
+                         `None`, this method returns a `unicode` object
+        :return: a `str` or `unicode` object
+        :rtype: `basestring`
+        :see: XMLSerializer, XHTMLSerializer, HTMLSerializer, TextSerializer
         """
+        from genshi.output import encode
         generator = self.serialize(method=method, **kwargs)
-        output = u''.join(list(generator))
-        if encoding is not None:
-            errors = 'replace'
-            if method != 'text':
-                errors = 'xmlcharrefreplace'
-            return output.encode(encoding, errors)
-        return output
+        return encode(generator, method=method, encoding=encoding)
 
     def select(self, path, namespaces=None, variables=None):
         """Return a new stream that contains the events matching the given
         XPath expression.
         
-        @param path: a string containing the XPath expression
+        :param path: a string containing the XPath expression
+        :param namespaces: mapping of namespace prefixes used in the path
+        :param variables: mapping of variable names to values
+        :return: the selected substream
+        :rtype: `Stream`
+        :raises PathSyntaxError: if the given path expression is invalid or not
+                                 supported
         """
         from genshi.path import Path
         return Path(path).select(self, namespaces, variables)
@@ -169,21 +185,19 @@
         the serialized output incrementally, as opposed to returning a single
         string.
         
-        @param method: determines how the stream is serialized; can be either
-                       "xml", "xhtml", "html", "text", or a custom serializer
-                       class
-
         Any additional keyword arguments are passed to the serializer, and thus
         depend on the `method` parameter value.
+        
+        :param method: determines how the stream is serialized; can be either
+                       "xml", "xhtml", "html", "text", or a custom serializer
+                       class
+        :return: an iterator over the serialization results (`Markup` or
+                 `unicode` objects, depending on the serialization method)
+        :rtype: ``iterator``
+        :see: XMLSerializer, XHTMLSerializer, HTMLSerializer, TextSerializer
         """
-        from genshi import output
-        cls = method
-        if isinstance(method, basestring):
-            cls = {'xml':   output.XMLSerializer,
-                   'xhtml': output.XHTMLSerializer,
-                   'html':  output.HTMLSerializer,
-                   'text':  output.TextSerializer}[method]
-        return cls(**kwargs)(_ensure(self))
+        from genshi.output import get_serializer
+        return get_serializer(method, **kwargs)(_ensure(self))
 
     def __str__(self):
         return self.render()
@@ -195,6 +209,7 @@
 START = Stream.START
 END = Stream.END
 TEXT = Stream.TEXT
+XML_DECL = Stream.XML_DECL
 DOCTYPE = Stream.DOCTYPE
 START_NS = Stream.START_NS
 END_NS = Stream.END_NS
@@ -217,30 +232,30 @@
 class Attrs(tuple):
     """Immutable sequence type that stores the attributes of an element.
     
-    Ordering of the attributes is preserved, while accessing by name is also
+    Ordering of the attributes is preserved, while access by name is also
     supported.
     
     >>> attrs = Attrs([('href', '#'), ('title', 'Foo')])
     >>> attrs
-    Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Foo')])
+    Attrs([('href', '#'), ('title', 'Foo')])
     
     >>> 'href' in attrs
     True
     >>> 'tabindex' in attrs
     False
-    >>> attrs.get(u'title')
+    >>> attrs.get('title')
     'Foo'
     
-    Instances may not be manipulated directly. Instead, the operators `|` and
-    `-` can be used to produce new instances that have specific attributes
+    Instances may not be manipulated directly. Instead, the operators ``|`` and
+    ``-`` can be used to produce new instances that have specific attributes
     added, replaced or removed.
     
-    To remove an attribute, use the `-` operator. The right hand side can be
+    To remove an attribute, use the ``-`` operator. The right hand side can be
     either a string or a set/sequence of strings, identifying the name(s) of
     the attribute(s) to remove:
     
     >>> attrs - 'title'
-    Attrs([(QName(u'href'), '#')])
+    Attrs([('href', '#')])
     >>> attrs - ('title', 'href')
     Attrs()
     
@@ -248,37 +263,32 @@
     used with an assignment:
 
     >>> attrs
-    Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Foo')])
+    Attrs([('href', '#'), ('title', 'Foo')])
     >>> attrs -= 'title'
     >>> attrs
-    Attrs([(QName(u'href'), '#')])
+    Attrs([('href', '#')])
     
-    To add a new attribute, use the `|` operator, where the right hand value
-    is a sequence of `(name, value)` tuples (which includes `Attrs` instances):
+    To add a new attribute, use the ``|`` operator, where the right hand value
+    is a sequence of ``(name, value)`` tuples (which includes `Attrs`
+    instances):
     
-    >>> attrs | [(u'title', 'Bar')]
-    Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Bar')])
+    >>> attrs | [('title', 'Bar')]
+    Attrs([('href', '#'), ('title', 'Bar')])
     
     If the attributes already contain an attribute with a given name, the value
     of that attribute is replaced:
     
-    >>> attrs | [(u'href', 'http://example.org/')]
-    Attrs([(QName(u'href'), 'http://example.org/')])
-    
+    >>> attrs | [('href', 'http://example.org/')]
+    Attrs([('href', 'http://example.org/')])
     """
     __slots__ = []
 
-    def __new__(cls, items=()):
-        """Create the `Attrs` instance.
-        
-        If the `items` parameter is provided, it is expected to be a sequence
-        of `(name, value)` tuples.
-        """
-        return tuple.__new__(cls, [(QName(name), val) for name, val in items])
-
     def __contains__(self, name):
         """Return whether the list includes an attribute with the specified
         name.
+        
+        :return: `True` if the list includes the attribute
+        :rtype: `bool`
         """
         for attr, _ in self:
             if attr == name:
@@ -290,6 +300,9 @@
     def __or__(self, attrs):
         """Return a new instance that contains the attributes in `attrs` in
         addition to any already existing attributes.
+        
+        :return: a new instance with the merged attributes
+        :rtype: `Attrs`
         """
         repl = dict([(an, av) for an, av in attrs if an in self])
         return Attrs([(sn, repl.get(sn, sv)) for sn, sv in self] +
@@ -303,6 +316,10 @@
     def __sub__(self, names):
         """Return a new instance with all attributes with a name in `names` are
         removed.
+        
+        :param names: the names of the attributes to remove
+        :return: a new instance with the attribute removed
+        :rtype: `Attrs`
         """
         if isinstance(names, basestring):
             names = (names,)
@@ -311,6 +328,12 @@
     def get(self, name, default=None):
         """Return the value of the attribute with the specified name, or the
         value of the `default` parameter if no such attribute is found.
+        
+        :param name: the name of the attribute
+        :param default: the value to return when the attribute does not exist
+        :return: the attribute value, or the `default` value if that attribute
+                 does not exist
+        :rtype: `object`
         """
         for attr, value in self:
             if attr == name:
@@ -320,8 +343,14 @@
     def totuple(self):
         """Return the attributes as a markup event.
         
-        The returned event is a TEXT event, the data is the value of all
+        The returned event is a `TEXT` event, the data is the value of all
         attributes joined together.
+        
+        >>> Attrs([('href', '#'), ('title', 'Foo')]).totuple()
+        ('TEXT', u'#Foo', (None, -1, -1))
+        
+        :return: a `TEXT` event
+        :rtype: `tuple`
         """
         return TEXT, u''.join([x[1] for x in self]), (None, -1, -1)
 
@@ -358,6 +387,20 @@
         return '<%s %r>' % (self.__class__.__name__, unicode(self))
 
     def join(self, seq, escape_quotes=True):
+        """Return a `Markup` object which is the concatenation of the strings
+        in the given sequence, where this `Markup` object is the separator
+        between the joined elements.
+        
+        Any element in the sequence that is not a `Markup` instance is
+        automatically escaped.
+        
+        :param seq: the sequence of strings to join
+        :param escape_quotes: whether double quote characters in the elements
+                              should be escaped
+        :return: the joined `Markup` object
+        :rtype: `Markup`
+        :see: `escape`
+        """
         return Markup(unicode(self).join([escape(item, quotes=escape_quotes)
                                           for item in seq]))
 
@@ -365,9 +408,21 @@
         """Create a Markup instance from a string and escape special characters
         it may contain (<, >, & and \").
         
+        >>> escape('"1 < 2"')
+        <Markup u'&#34;1 &lt; 2&#34;'>
+        
         If the `quotes` parameter is set to `False`, the \" character is left
         as is. Escaping quotes is generally only required for strings that are
         to be used in attribute values.
+        
+        >>> escape('"1 < 2"', quotes=False)
+        <Markup u'"1 &lt; 2"'>
+        
+        :param text: the text to escape
+        :param quotes: if ``True``, double quote characters are escaped in
+                       addition to the other special characters
+        :return: the escaped `Markup` string
+        :rtype: `Markup`
         """
         if not text:
             return cls()
@@ -382,7 +437,15 @@
     escape = classmethod(escape)
 
     def unescape(self):
-        """Reverse-escapes &, <, > and \" and returns a `unicode` object."""
+        """Reverse-escapes &, <, >, and \" and returns a `unicode` object.
+        
+        >>> Markup('1 &lt; 2').unescape()
+        u'1 < 2'
+        
+        :return: the unescaped string
+        :rtype: `unicode`
+        :see: `genshi.core.unescape`
+        """
         if not self:
             return u''
         return unicode(self).replace('&#34;', '"') \
@@ -395,20 +458,43 @@
         replaced by the equivalent UTF-8 characters.
         
         If the `keepxmlentities` parameter is provided and evaluates to `True`,
-        the core XML entities (&amp;, &apos;, &gt;, &lt; and &quot;) are not
-        stripped.
+        the core XML entities (``&amp;``, ``&apos;``, ``&gt;``, ``&lt;`` and
+        ``&quot;``) are not stripped.
+        
+        :return: a `Markup` instance with entities removed
+        :rtype: `Markup`
+        :see: `genshi.util.stripentities`
         """
         return Markup(stripentities(self, keepxmlentities=keepxmlentities))
 
     def striptags(self):
-        """Return a copy of the text with all XML/HTML tags removed."""
+        """Return a copy of the text with all XML/HTML tags removed.
+        
+        :return: a `Markup` instance with all tags removed
+        :rtype: `Markup`
+        :see: `genshi.util.striptags`
+        """
         return Markup(striptags(self))
 
 
 escape = Markup.escape
 
 def unescape(text):
-    """Reverse-escapes &, <, > and \" and returns a `unicode` object."""
+    """Reverse-escapes &, <, >, and \" and returns a `unicode` object.
+    
+    >>> unescape(Markup('1 &lt; 2'))
+    u'1 < 2'
+    
+    If the provided `text` object is not a `Markup` instance, it is returned
+    unchanged.
+    
+    >>> unescape('1 &lt; 2')
+    '1 &lt; 2'
+    
+    :param text: the text to unescape
+    :return: the unescsaped string
+    :rtype: `unicode`
+    """
     if not isinstance(text, Markup):
         return text
     return text.unescape()
@@ -446,7 +532,7 @@
     QName(u'http://www.w3.org/1999/xhtml}body')
     
     A `Namespace` object can also be used to test whether a specific `QName`
-    belongs to that namespace using the `in` operator:
+    belongs to that namespace using the ``in`` operator:
     
     >>> qname = html.body
     >>> qname in html
@@ -504,7 +590,7 @@
     """A qualified element or attribute name.
     
     The unicode value of instances of this class contains the qualified name of
-    the element or attribute, in the form `{namespace}localname`. The namespace
+    the element or attribute, in the form ``{namespace}localname``. The namespace
     URI can be obtained through the additional `namespace` attribute, while the
     local name can be accessed through the `localname` attribute.
     
deleted file mode 100644
--- a/genshi/filters.py
+++ /dev/null
@@ -1,286 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
-
-"""Implementation of a number of stream filters."""
-
-try:
-    frozenset
-except NameError:
-    from sets import ImmutableSet as frozenset
-import re
-
-from genshi.core import Attrs, stripentities
-from genshi.core import END, START, TEXT
-
-__all__ = ['HTMLFormFiller', 'HTMLSanitizer']
-
-
-class HTMLFormFiller(object):
-    """A stream filter that can populate HTML forms from a dictionary of values.
-    
-    >>> from genshi.input import HTML
-    >>> html = HTML('''<form>
-    ...   <p><input type="text" name="foo" /></p>
-    ... </form>''')
-    >>> filler = HTMLFormFiller(data={'foo': 'bar'})
-    >>> print html | filler
-    <form>
-      <p><input type="text" name="foo" value="bar"/></p>
-    </form>
-    """
-    # TODO: only select the first radio button, and the first select option
-    #       (if not in a multiple-select)
-    # TODO: only apply to elements in the XHTML namespace (or no namespace)?
-
-    def __init__(self, name=None, id=None, data=None):
-        """Create the filter.
-        
-        @param name: The name of the form that should be populated. If this
-            parameter is given, only forms where the ``name`` attribute value
-            matches the parameter are processed.
-        @param id: The ID of the form that should be populated. If this
-            parameter is given, only forms where the ``id`` attribute value
-            matches the parameter are processed.
-        @param data: The dictionary of form values, where the keys are the names
-            of the form fields, and the values are the values to fill in.
-        """
-        self.name = name
-        self.id = id
-        if data is None:
-            data = {}
-        self.data = data
-
-    def __call__(self, stream, ctxt=None):
-        """Apply the filter to the given stream.
-        
-        @param stream: the markup event stream to filter
-        @param ctxt: the template context (unused)
-        """
-        in_form = in_select = in_option = in_textarea = False
-        select_value = option_value = textarea_value = None
-        option_start = option_text = None
-
-        for kind, data, pos in stream:
-
-            if kind is START:
-                tag, attrs = data
-                tagname = tag.localname
-
-                if tagname == 'form' and (
-                        self.name and attrs.get('name') == self.name or
-                        self.id and attrs.get('id') == self.id or
-                        not (self.id or self.name)):
-                    in_form = True
-
-                elif in_form:
-                    if tagname == 'input':
-                        type = attrs.get('type')
-                        if type in ('checkbox', 'radio'):
-                            name = attrs.get('name')
-                            if name:
-                                value = self.data.get(name)
-                                declval = attrs.get('value')
-                                checked = False
-                                if isinstance(value, (list, tuple)):
-                                    if declval:
-                                        checked = declval in value
-                                    else:
-                                        checked = bool(filter(None, value))
-                                else:
-                                    if declval:
-                                        checked = declval == value
-                                    elif type == 'checkbox':
-                                        checked = bool(value)
-                                if checked:
-                                    attrs |= [('checked', 'checked')]
-                                elif 'checked' in attrs:
-                                    attrs -= 'checked'
-                        elif type in (None, 'hidden', 'text'):
-                            name = attrs.get('name')
-                            if name:
-                                value = self.data.get(name)
-                                if isinstance(value, (list, tuple)):
-                                    value = value[0]
-                                if value is not None:
-                                    attrs |= [('value', unicode(value))]
-                    elif tagname == 'select':
-                        name = attrs.get('name')
-                        select_value = self.data.get(name)
-                        in_select = True
-                    elif tagname == 'textarea':
-                        name = attrs.get('name')
-                        textarea_value = self.data.get(name)
-                        if isinstance(textarea_value, (list, tuple)):
-                            textarea_value = textarea_value[0]
-                        in_textarea = True
-                    elif in_select and tagname == 'option':
-                        option_start = kind, data, pos
-                        option_value = attrs.get('value')
-                        in_option = True
-                        continue
-                yield kind, (tag, attrs), pos
-
-
-            elif in_form and kind is TEXT:
-                if in_select and in_option:
-                    if option_value is None:
-                        option_value = data
-                    option_text = kind, data, pos
-                    continue
-                elif in_textarea:
-                    continue
-                yield kind, data, pos
-
-            elif in_form and kind is END:
-                tagname = data.localname
-                if tagname == 'form':
-                    in_form = False
-                elif tagname == 'select':
-                    in_select = False
-                    select_value = None
-                elif in_select and tagname == 'option':
-                    if isinstance(select_value, (tuple, list)):
-                        selected = option_value in select_value
-                    else:
-                        selected = option_value == select_value
-                    okind, (tag, attrs), opos = option_start
-                    if selected:
-                        attrs |= [('selected', 'selected')]
-                    elif 'selected' in attrs:
-                        attrs -= 'selected'
-                    yield okind, (tag, attrs), opos
-                    if option_text:
-                        yield option_text
-                    in_option = False
-                    option_start = option_text = option_value = None
-                elif tagname == 'textarea':
-                    if textarea_value:
-                        yield TEXT, unicode(textarea_value), pos
-                    in_textarea = False
-                yield kind, data, pos
-
-            else:
-                yield kind, data, pos
-
-
-class HTMLSanitizer(object):
-    """A filter that removes potentially dangerous HTML tags and attributes
-    from the stream.
-    """
-
-    SAFE_TAGS = frozenset(['a', 'abbr', 'acronym', 'address', 'area', 'b',
-        'big', 'blockquote', 'br', 'button', 'caption', 'center', 'cite',
-        'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt',
-        'em', 'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
-        'hr', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'map',
-        'menu', 'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'samp',
-        'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
-        'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u',
-        'ul', 'var'])
-
-    SAFE_ATTRS = frozenset(['abbr', 'accept', 'accept-charset', 'accesskey',
-        'action', 'align', 'alt', 'axis', 'bgcolor', 'border', 'cellpadding',
-        'cellspacing', 'char', 'charoff', 'charset', 'checked', 'cite', 'class',
-        'clear', 'cols', 'colspan', 'color', 'compact', 'coords', 'datetime',
-        'dir', 'disabled', 'enctype', 'for', 'frame', 'headers', 'height',
-        'href', 'hreflang', 'hspace', 'id', 'ismap', 'label', 'lang',
-        'longdesc', 'maxlength', 'media', 'method', 'multiple', 'name',
-        'nohref', 'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev',
-        'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
-        'span', 'src', 'start', 'style', 'summary', 'tabindex', 'target',
-        'title', 'type', 'usemap', 'valign', 'value', 'vspace', 'width'])
-
-    SAFE_SCHEMES = frozenset(['file', 'ftp', 'http', 'https', 'mailto', None])
-
-    URI_ATTRS = frozenset(['action', 'background', 'dynsrc', 'href', 'lowsrc',
-        'src'])
-
-    def __init__(self, safe_tags=SAFE_TAGS, safe_attrs=SAFE_ATTRS,
-                 safe_schemes=SAFE_SCHEMES, uri_attrs=URI_ATTRS):
-        """Create the sanitizer.
-        
-        The exact set of allowed elements and attributes can be configured.
-        
-        @param safe_tags: a set of tag names that are considered safe
-        @param safe_attrs: a set of attribute names that are considered safe
-        @param safe_schemes: a set of URI schemes that are considered safe
-        @param uri_attrs: a set of names of attributes that contain URIs
-        """
-        self.safe_tags = safe_tags
-        self.safe_attrs = safe_attrs
-        self.uri_attrs = uri_attrs
-        self.safe_schemes = safe_schemes
-
-    def __call__(self, stream, ctxt=None):
-        """Apply the filter to the given stream.
-        
-        @param stream: the markup event stream to filter
-        @param ctxt: the template context (unused)
-        """
-        waiting_for = None
-
-        def _get_scheme(href):
-            if ':' not in href:
-                return None
-            chars = [char for char in href.split(':', 1)[0] if char.isalnum()]
-            return ''.join(chars).lower()
-
-        for kind, data, pos in stream:
-            if kind is START:
-                if waiting_for:
-                    continue
-                tag, attrs = data
-                if tag not in self.safe_tags:
-                    waiting_for = tag
-                    continue
-
-                new_attrs = []
-                for attr, value in attrs:
-                    value = stripentities(value)
-                    if attr not in self.safe_attrs:
-                        continue
-                    elif attr in self.uri_attrs:
-                        # Don't allow URI schemes such as "javascript:"
-                        if _get_scheme(value) not in self.safe_schemes:
-                            continue
-                    elif attr == 'style':
-                        # Remove dangerous CSS declarations from inline styles
-                        decls = []
-                        for decl in filter(None, value.split(';')):
-                            is_evil = False
-                            if 'expression' in decl:
-                                is_evil = True
-                            for m in re.finditer(r'url\s*\(([^)]+)', decl):
-                                if _get_scheme(m.group(1)) not in self.safe_schemes:
-                                    is_evil = True
-                                    break
-                            if not is_evil:
-                                decls.append(decl.strip())
-                        if not decls:
-                            continue
-                        value = '; '.join(decls)
-                    new_attrs.append((attr, value))
-
-                yield kind, (tag, Attrs(new_attrs)), pos
-
-            elif kind is END:
-                tag = data
-                if waiting_for:
-                    if waiting_for == tag:
-                        waiting_for = None
-                else:
-                    yield kind, data, pos
-
-            else:
-                if not waiting_for:
-                    yield kind, data, pos
new file mode 100644
--- /dev/null
+++ b/genshi/filters/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+"""Implementation of a number of stream filters."""
+
+from genshi.filters.html import HTMLFormFiller, HTMLSanitizer
+from genshi.filters.i18n import Translator
+
+__docformat__ = 'restructuredtext en'
new file mode 100644
--- /dev/null
+++ b/genshi/filters/html.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2007 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+"""Implementation of a number of stream filters."""
+
+try:
+    frozenset
+except NameError:
+    from sets import ImmutableSet as frozenset
+import re
+
+from genshi.core import Attrs, QName, stripentities
+from genshi.core import END, START, TEXT
+
+__all__ = ['HTMLFormFiller', 'HTMLSanitizer']
+__docformat__ = 'restructuredtext en'
+
+
+class HTMLFormFiller(object):
+    """A stream filter that can populate HTML forms from a dictionary of values.
+    
+    >>> from genshi.input import HTML
+    >>> html = HTML('''<form>
+    ...   <p><input type="text" name="foo" /></p>
+    ... </form>''')
+    >>> filler = HTMLFormFiller(data={'foo': 'bar'})
+    >>> print html | filler
+    <form>
+      <p><input type="text" name="foo" value="bar"/></p>
+    </form>
+    """
+    # TODO: only select the first radio button, and the first select option
+    #       (if not in a multiple-select)
+    # TODO: only apply to elements in the XHTML namespace (or no namespace)?
+
+    def __init__(self, name=None, id=None, data=None):
+        """Create the filter.
+        
+        :param name: The name of the form that should be populated. If this
+                     parameter is given, only forms where the ``name`` attribute
+                     value matches the parameter are processed.
+        :param id: The ID of the form that should be populated. If this
+                   parameter is given, only forms where the ``id`` attribute
+                   value matches the parameter are processed.
+        :param data: The dictionary of form values, where the keys are the names
+                     of the form fields, and the values are the values to fill
+                     in.
+        """
+        self.name = name
+        self.id = id
+        if data is None:
+            data = {}
+        self.data = data
+
+    def __call__(self, stream):
+        """Apply the filter to the given stream.
+        
+        :param stream: the markup event stream to filter
+        """
+        in_form = in_select = in_option = in_textarea = False
+        select_value = option_value = textarea_value = None
+        option_start = option_text = None
+
+        for kind, data, pos in stream:
+
+            if kind is START:
+                tag, attrs = data
+                tagname = tag.localname
+
+                if tagname == 'form' and (
+                        self.name and attrs.get('name') == self.name or
+                        self.id and attrs.get('id') == self.id or
+                        not (self.id or self.name)):
+                    in_form = True
+
+                elif in_form:
+                    if tagname == 'input':
+                        type = attrs.get('type')
+                        if type in ('checkbox', 'radio'):
+                            name = attrs.get('name')
+                            if name and name in self.data:
+                                value = self.data[name]
+                                declval = attrs.get('value')
+                                checked = False
+                                if isinstance(value, (list, tuple)):
+                                    if declval:
+                                        checked = declval in [str(v) for v
+                                                              in value]
+                                    else:
+                                        checked = bool(filter(None, value))
+                                else:
+                                    if declval:
+                                        checked = declval == str(value)
+                                    elif type == 'checkbox':
+                                        checked = bool(value)
+                                if checked:
+                                    attrs |= [(QName('checked'), 'checked')]
+                                elif 'checked' in attrs:
+                                    attrs -= 'checked'
+                        elif type in (None, 'hidden', 'text'):
+                            name = attrs.get('name')
+                            if name and name in self.data:
+                                value = self.data[name]
+                                if isinstance(value, (list, tuple)):
+                                    value = value[0]
+                                if value is not None:
+                                    attrs |= [(QName('value'), unicode(value))]
+                    elif tagname == 'select':
+                        name = attrs.get('name')
+                        if name in self.data:
+                            select_value = self.data[name]
+                            in_select = True
+                    elif tagname == 'textarea':
+                        name = attrs.get('name')
+                        if name in self.data:
+                            textarea_value = self.data.get(name)
+                            if isinstance(textarea_value, (list, tuple)):
+                                textarea_value = textarea_value[0]
+                            in_textarea = True
+                    elif in_select and tagname == 'option':
+                        option_start = kind, data, pos
+                        option_value = attrs.get('value')
+                        in_option = True
+                        continue
+                yield kind, (tag, attrs), pos
+
+            elif in_form and kind is TEXT:
+                if in_select and in_option:
+                    if option_value is None:
+                        option_value = data
+                    option_text = kind, data, pos
+                    continue
+                elif in_textarea:
+                    continue
+                yield kind, data, pos
+
+            elif in_form and kind is END:
+                tagname = data.localname
+                if tagname == 'form':
+                    in_form = False
+                elif tagname == 'select':
+                    in_select = False
+                    select_value = None
+                elif in_select and tagname == 'option':
+                    if isinstance(select_value, (tuple, list)):
+                        selected = option_value in [str(v) for v
+                                                    in select_value]
+                    else:
+                        selected = option_value == str(select_value)
+                    okind, (tag, attrs), opos = option_start
+                    if selected:
+                        attrs |= [(QName('selected'), 'selected')]
+                    elif 'selected' in attrs:
+                        attrs -= 'selected'
+                    yield okind, (tag, attrs), opos
+                    if option_text:
+                        yield option_text
+                    in_option = False
+                    option_start = option_text = option_value = None
+                elif tagname == 'textarea':
+                    if textarea_value:
+                        yield TEXT, unicode(textarea_value), pos
+                    in_textarea = False
+                yield kind, data, pos
+
+            else:
+                yield kind, data, pos
+
+
+class HTMLSanitizer(object):
+    """A filter that removes potentially dangerous HTML tags and attributes
+    from the stream.
+    
+    >>> from genshi import HTML
+    >>> html = HTML('<div><script>alert(document.cookie)</script></div>')
+    >>> print html | HTMLSanitizer()
+    <div/>
+    
+    The default set of safe tags and attributes can be modified when the filter
+    is instantiated. For example, to allow inline ``style`` attributes, the
+    following instantation would work:
+    
+    >>> html = HTML('<div style="background: #000"></div>')
+    >>> sanitizer = HTMLSanitizer(safe_attrs=HTMLSanitizer.SAFE_ATTRS | set(['style']))
+    >>> print html | sanitizer
+    <div style="background: #000"/>
+    
+    Note that even in this case, the filter *does* attempt to remove dangerous
+    constructs from style attributes:
+
+    >>> html = HTML('<div style="background: url(javascript:void); color: #000"></div>')
+    >>> print html | sanitizer
+    <div style="color: #000"/>
+    
+    This handles HTML entities, unicode escapes in CSS and Javascript text, as
+    well as a lot of other things. However, the style tag is still excluded by
+    default because it is very hard for such sanitizing to be completely safe,
+    especially considering how much error recovery current web browsers perform.
+    """
+
+    SAFE_TAGS = frozenset(['a', 'abbr', 'acronym', 'address', 'area', 'b',
+        'big', 'blockquote', 'br', 'button', 'caption', 'center', 'cite',
+        'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt',
+        'em', 'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+        'hr', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'map',
+        'menu', 'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'samp',
+        'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
+        'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u',
+        'ul', 'var'])
+
+    SAFE_ATTRS = frozenset(['abbr', 'accept', 'accept-charset', 'accesskey',
+        'action', 'align', 'alt', 'axis', 'bgcolor', 'border', 'cellpadding',
+        'cellspacing', 'char', 'charoff', 'charset', 'checked', 'cite', 'class',
+        'clear', 'cols', 'colspan', 'color', 'compact', 'coords', 'datetime',
+        'dir', 'disabled', 'enctype', 'for', 'frame', 'headers', 'height',
+        'href', 'hreflang', 'hspace', 'id', 'ismap', 'label', 'lang',
+        'longdesc', 'maxlength', 'media', 'method', 'multiple', 'name',
+        'nohref', 'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev',
+        'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
+        'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title',
+        'type', 'usemap', 'valign', 'value', 'vspace', 'width'])
+
+    SAFE_SCHEMES = frozenset(['file', 'ftp', 'http', 'https', 'mailto', None])
+
+    URI_ATTRS = frozenset(['action', 'background', 'dynsrc', 'href', 'lowsrc',
+        'src'])
+
+    def __init__(self, safe_tags=SAFE_TAGS, safe_attrs=SAFE_ATTRS,
+                 safe_schemes=SAFE_SCHEMES, uri_attrs=URI_ATTRS):
+        """Create the sanitizer.
+        
+        The exact set of allowed elements and attributes can be configured.
+        
+        :param safe_tags: a set of tag names that are considered safe
+        :param safe_attrs: a set of attribute names that are considered safe
+        :param safe_schemes: a set of URI schemes that are considered safe
+        :param uri_attrs: a set of names of attributes that contain URIs
+        """
+        self.safe_tags = safe_tags
+        self.safe_attrs = safe_attrs
+        self.uri_attrs = uri_attrs
+        self.safe_schemes = safe_schemes
+
+    def __call__(self, stream):
+        """Apply the filter to the given stream.
+        
+        :param stream: the markup event stream to filter
+        """
+        waiting_for = None
+
+        def _get_scheme(href):
+            if ':' not in href:
+                return None
+            chars = [char for char in href.split(':', 1)[0] if char.isalnum()]
+            return ''.join(chars).lower()
+
+        for kind, data, pos in stream:
+            if kind is START:
+                if waiting_for:
+                    continue
+                tag, attrs = data
+                if tag not in self.safe_tags:
+                    waiting_for = tag
+                    continue
+
+                new_attrs = []
+                for attr, value in attrs:
+                    value = stripentities(value)
+                    if attr not in self.safe_attrs:
+                        continue
+                    elif attr in self.uri_attrs:
+                        # Don't allow URI schemes such as "javascript:"
+                        if _get_scheme(value) not in self.safe_schemes:
+                            continue
+                    elif attr == 'style':
+                        # Remove dangerous CSS declarations from inline styles
+                        decls = []
+                        value = self._replace_unicode_escapes(value)
+                        for decl in filter(None, value.split(';')):
+                            is_evil = False
+                            if 'expression' in decl:
+                                is_evil = True
+                            for m in re.finditer(r'url\s*\(([^)]+)', decl):
+                                if _get_scheme(m.group(1)) not in self.safe_schemes:
+                                    is_evil = True
+                                    break
+                            if not is_evil:
+                                decls.append(decl.strip())
+                        if not decls:
+                            continue
+                        value = '; '.join(decls)
+                    new_attrs.append((attr, value))
+
+                yield kind, (tag, Attrs(new_attrs)), pos
+
+            elif kind is END:
+                tag = data
+                if waiting_for:
+                    if waiting_for == tag:
+                        waiting_for = None
+                else:
+                    yield kind, data, pos
+
+            else:
+                if not waiting_for:
+                    yield kind, data, pos
+
+    _NORMALIZE_NEWLINES = re.compile(r'\r\n').sub
+    _UNICODE_ESCAPE = re.compile(r'\\([0-9a-fA-F]{1,6})\s?').sub
+
+    def _replace_unicode_escapes(self, text):
+        def _repl(match):
+            return unichr(int(match.group(1), 16))
+        return self._UNICODE_ESCAPE(_repl, self._NORMALIZE_NEWLINES('\n', text))
new file mode 100644
--- /dev/null
+++ b/genshi/filters/i18n.py
@@ -0,0 +1,286 @@
+"""Utilities for internationalization and localization of templates."""
+
+try:
+    frozenset
+except NameError:
+    from sets import ImmutableSet as frozenset
+from gettext import gettext
+from opcode import opmap
+import re
+
+from genshi.core import Attrs, Namespace, QName, START, END, TEXT, _ensure
+from genshi.template.base import Template, EXPR, SUB
+from genshi.template.markup import EXEC
+
+_LOAD_NAME = chr(opmap['LOAD_NAME'])
+_LOAD_CONST = chr(opmap['LOAD_CONST'])
+_CALL_FUNCTION = chr(opmap['CALL_FUNCTION'])
+_BINARY_ADD = chr(opmap['BINARY_ADD'])
+
+
+class Translator(object):
+    """Can extract and translate localizable strings from markup streams and
+    templates.
+    
+    For example, assume the followng template:
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> 
+    >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
+    ...   <head>
+    ...     <title>Example</title>
+    ...   </head>
+    ...   <body>
+    ...     <h1>Example</h1>
+    ...     <p>${_("Hello, %(name)s") % dict(name=username)}</p>
+    ...   </body>
+    ... </html>''', filename='example.html')
+    
+    For demonstration, we define a dummy ``gettext``-style function with a
+    hard-coded translation table, and pass that to the `Translator` initializer:
+    
+    >>> def pseudo_gettext(string):
+    ...     return {
+    ...         'Example': 'Beispiel',
+    ...         'Hello, %(name)s': 'Hallo, %(name)s'
+    ...     }[string]
+    >>> 
+    >>> translator = Translator(pseudo_gettext)
+    
+    Next, the translator needs to be prepended to any already defined filters
+    on the template:
+    
+    >>> tmpl.filters.insert(0, translator)
+    
+    When generating the template output, our hard-coded translations should be
+    applied as expected:
+    
+    >>> print tmpl.generate(username='Hans', _=pseudo_gettext)
+    <html>
+      <head>
+        <title>Beispiel</title>
+      </head>
+      <body>
+        <h1>Beispiel</h1>
+        <p>Hallo, Hans</p>
+      </body>
+    </html>
+    """
+
+    IGNORE_TAGS = frozenset([
+        QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
+        QName('style'), QName('http://www.w3.org/1999/xhtml}style')
+    ])
+    INCLUDE_ATTRS = frozenset(['abbr', 'alt', 'label', 'prompt', 'standby',
+                               'summary', 'title'])
+
+    def __init__(self, translate=gettext, ignore_tags=IGNORE_TAGS,
+                 include_attrs=INCLUDE_ATTRS):
+        """Initialize the translator.
+        
+        :param translate: the translation function, for example ``gettext`` or
+                          ``ugettext``.
+        :param ignore_tags: a set of tag names that should not be localized
+        :param include_attrs: a set of attribute names should be localized
+        """
+        self.translate = translate
+        self.ignore_tags = ignore_tags
+        self.include_attrs = include_attrs
+
+    def __call__(self, stream, ctxt=None, search_text=True):
+        """Translate any localizable strings in the given stream.
+        
+        This function shouldn't be called directly. Instead, an instance of
+        the `Translator` class should be registered as a filter with the
+        `Template` or the `TemplateLoader`, or applied as a regular stream
+        filter. If used as a template filter, it should be inserted in front of
+        all the default filters.
+        
+        :param stream: the markup event stream
+        :param ctxt: the template context (not used)
+        :param search_text: whether text nodes should be translated (used
+                            internally)
+        :return: the localized stream
+        """
+        ignore_tags = self.ignore_tags
+        include_attrs = self.include_attrs
+        translate = self.translate
+        skip = 0
+
+        for kind, data, pos in stream:
+
+            # skip chunks that should not be localized
+            if skip:
+                if kind is START:
+                    tag, attrs = data
+                    if tag in ignore_tags:
+                        skip += 1
+                elif kind is END:
+                    if tag in ignore_tags:
+                        skip -= 1
+                yield kind, data, pos
+                continue
+
+            # handle different events that can be localized
+            if kind is START:
+                tag, attrs = data
+                if tag in ignore_tags:
+                    skip += 1
+                    yield kind, data, pos
+                    continue
+
+                new_attrs = []
+                changed = False
+                for name, value in attrs:
+                    newval = value
+                    if isinstance(value, basestring):
+                        if name in include_attrs:
+                            newval = self.translate(value)
+                    else:
+                        newval = list(self(_ensure(value), ctxt,
+                            search_text=name in include_attrs)
+                        )
+                    if newval != value:
+                        value = newval
+                        changed = True
+                    new_attrs.append((name, value))
+                if changed:
+                    attrs = new_attrs
+
+                yield kind, (tag, attrs), pos
+
+            elif search_text and kind is TEXT:
+                text = data.strip()
+                if text:
+                    data = data.replace(text, translate(text))
+                yield kind, data, pos
+
+            elif kind is SUB:
+                subkind, substream = data
+                new_substream = list(self(substream, ctxt))
+                yield kind, (subkind, new_substream), pos
+
+            else:
+                yield kind, data, pos
+
+    GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext', 'dgettext', 'dngettext',
+                         'ugettext', 'ungettext')
+
+    def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS,
+                search_text=True):
+        """Extract localizable strings from the given template stream.
+        
+        For every string found, this function yields a ``(lineno, function,
+        message)`` tuple, where:
+        
+        * ``lineno`` is the number of the line on which the string was found,
+        * ``function`` is the name of the ``gettext`` function used (if the
+          string was extracted from embedded Python code), and
+        *  ``message`` is the string itself (a ``unicode`` object, or a tuple
+           of ``unicode`` objects for functions with multiple string arguments).
+        
+        >>> from genshi.template import MarkupTemplate
+        >>> 
+        >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
+        ...   <head>
+        ...     <title>Example</title>
+        ...   </head>
+        ...   <body>
+        ...     <h1>Example</h1>
+        ...     <p>${_("Hello, %(name)s") % dict(name=username)}</p>
+        ...     <p>${ngettext("You have %d item", "You have %d items", num)}</p>
+        ...   </body>
+        ... </html>''', filename='example.html')
+        >>> 
+        >>> for lineno, funcname, message in Translator().extract(tmpl.stream):
+        ...    print "%d, %r, %r" % (lineno, funcname, message)
+        3, None, u'Example'
+        6, None, u'Example'
+        7, '_', u'Hello, %(name)s'
+        8, 'ngettext', (u'You have %d item', u'You have %d items')
+        
+        :param stream: the event stream to extract strings from; can be a
+                       regular stream or a template stream
+        :param gettext_functions: a sequence of function names that should be
+                                  treated as gettext-style localization
+                                  functions
+        :param search_text: whether the content of text nodes should be
+                            extracted (used internally)
+        
+        :note: Changed in 0.4.1: For a function with multiple string arguments
+               (such as ``ngettext``), a single item with a tuple of strings is
+               yielded, instead an item for each string argument.
+        """
+        tagname = None
+        skip = 0
+
+        for kind, data, pos in stream:
+            if skip:
+                if kind is START:
+                    tag, attrs = data
+                    if tag in self.ignore_tags:
+                        skip += 1
+                if kind is END:
+                    tag = data
+                    if tag in self.ignore_tags:
+                        skip -= 1
+                continue
+
+            if kind is START:
+                tag, attrs = data
+                if tag in self.ignore_tags:
+                    skip += 1
+                    continue
+
+                for name, value in attrs:
+                    if isinstance(value, basestring):
+                        if name in self.include_attrs:
+                            text = value.strip()
+                            if text:
+                                yield pos[1], None, text
+                    else:
+                        for lineno, funcname, text in self.extract(
+                                _ensure(value), gettext_functions,
+                                search_text=name in self.include_attrs):
+                            yield lineno, funcname, text
+
+            elif search_text and kind is TEXT:
+                text = data.strip()
+                if text and filter(None, [ch.isalpha() for ch in text]):
+                    yield pos[1], None, text
+
+            elif kind is EXPR or kind is EXEC:
+                consts = dict([(n, chr(i) + '\x00') for i, n in
+                               enumerate(data.code.co_consts)])
+                gettext_locs = [consts[n] for n in gettext_functions
+                                if n in consts]
+                ops = [
+                    _LOAD_CONST, '(', '|'.join(gettext_locs), ')',
+                    _CALL_FUNCTION, '.\x00',
+                    '((?:', _BINARY_ADD, '|', _LOAD_CONST, '.\x00)+)'
+                ]
+                for loc, opcodes in re.findall(''.join(ops), data.code.co_code):
+                    funcname = data.code.co_consts[ord(loc[0])]
+                    strings = []
+                    opcodes = iter(opcodes)
+                    for opcode in opcodes:
+                        if opcode == _BINARY_ADD:
+                            arg = strings.pop()
+                            strings[-1] += arg
+                        else:
+                            arg = data.code.co_consts[ord(opcodes.next())]
+                            opcodes.next() # skip second byte
+                            if not isinstance(arg, basestring):
+                                break
+                            strings.append(unicode(arg))
+                    if len(strings) == 1:
+                        strings = strings[0]
+                    else:
+                        strings = tuple(strings)
+                    yield pos[1], funcname, strings
+
+            elif kind is SUB:
+                subkind, substream = data
+                for lineno, funcname, text in self.extract(substream,
+                                                           gettext_functions):
+                    yield lineno, funcname, text
new file mode 100644
--- /dev/null
+++ b/genshi/filters/tests/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+import doctest
+import unittest
+
+def suite():
+    from genshi.filters.tests import html, i18n
+    suite = unittest.TestSuite()
+    suite.addTest(html.suite())
+    suite.addTest(i18n.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/filters/tests/html.py
@@ -0,0 +1,392 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from genshi.input import HTML, ParseError
+from genshi.filters.html import HTMLFormFiller, HTMLSanitizer
+
+
+class HTMLFormFillerTestCase(unittest.TestCase):
+
+    def test_fill_input_text_no_value(self):
+        html = HTML("""<form><p>
+          <input type="text" name="foo" />
+        </p></form>""") | HTMLFormFiller()
+        self.assertEquals("""<form><p>
+          <input type="text" name="foo"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_input_text_single_value(self):
+        html = HTML("""<form><p>
+          <input type="text" name="foo" />
+        </p></form>""") | HTMLFormFiller(data={'foo': 'bar'})
+        self.assertEquals("""<form><p>
+          <input type="text" name="foo" value="bar"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_input_text_multi_value(self):
+        html = HTML("""<form><p>
+          <input type="text" name="foo" />
+        </p></form>""") | HTMLFormFiller(data={'foo': ['bar']})
+        self.assertEquals("""<form><p>
+          <input type="text" name="foo" value="bar"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_input_hidden_no_value(self):
+        html = HTML("""<form><p>
+          <input type="hidden" name="foo" />
+        </p></form>""") | HTMLFormFiller()
+        self.assertEquals("""<form><p>
+          <input type="hidden" name="foo"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_input_hidden_single_value(self):
+        html = HTML("""<form><p>
+          <input type="hidden" name="foo" />
+        </p></form>""") | HTMLFormFiller(data={'foo': 'bar'})
+        self.assertEquals("""<form><p>
+          <input type="hidden" name="foo" value="bar"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_input_hidden_multi_value(self):
+        html = HTML("""<form><p>
+          <input type="hidden" name="foo" />
+        </p></form>""") | HTMLFormFiller(data={'foo': ['bar']})
+        self.assertEquals("""<form><p>
+          <input type="hidden" name="foo" value="bar"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_textarea_no_value(self):
+        html = HTML("""<form><p>
+          <textarea name="foo"></textarea>
+        </p></form>""") | HTMLFormFiller()
+        self.assertEquals("""<form><p>
+          <textarea name="foo"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_textarea_single_value(self):
+        html = HTML("""<form><p>
+          <textarea name="foo"></textarea>
+        </p></form>""") | HTMLFormFiller(data={'foo': 'bar'})
+        self.assertEquals("""<form><p>
+          <textarea name="foo">bar</textarea>
+        </p></form>""", unicode(html))
+
+    def test_fill_textarea_multi_value(self):
+        html = HTML("""<form><p>
+          <textarea name="foo"></textarea>
+        </p></form>""") | HTMLFormFiller(data={'foo': ['bar']})
+        self.assertEquals("""<form><p>
+          <textarea name="foo">bar</textarea>
+        </p></form>""", unicode(html))
+
+    def test_fill_input_checkbox_no_value(self):
+        html = HTML("""<form><p>
+          <input type="checkbox" name="foo" />
+        </p></form>""") | HTMLFormFiller()
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_input_checkbox_single_value_auto(self):
+        html = HTML("""<form><p>
+          <input type="checkbox" name="foo" />
+        </p></form>""")
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ''})))
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo" checked="checked"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': 'on'})))
+
+    def test_fill_input_checkbox_single_value_defined(self):
+        html = HTML("""<form><p>
+          <input type="checkbox" name="foo" value="1" />
+        </p></form>""")
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo" value="1" checked="checked"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': '1'})))
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo" value="1"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': '2'})))
+
+    def test_fill_input_checkbox_multi_value_auto(self):
+        html = HTML("""<form><p>
+          <input type="checkbox" name="foo" />
+        </p></form>""")
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': []})))
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo" checked="checked"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['on']})))
+
+    def test_fill_input_checkbox_multi_value_defined(self):
+        html = HTML("""<form><p>
+          <input type="checkbox" name="foo" value="1" />
+        </p></form>""")
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo" value="1" checked="checked"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['1']})))
+        self.assertEquals("""<form><p>
+          <input type="checkbox" name="foo" value="1"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['2']})))
+
+    def test_fill_input_radio_no_value(self):
+        html = HTML("""<form><p>
+          <input type="radio" name="foo" />
+        </p></form>""") | HTMLFormFiller()
+        self.assertEquals("""<form><p>
+          <input type="radio" name="foo"/>
+        </p></form>""", unicode(html))
+
+    def test_fill_input_radio_single_value(self):
+        html = HTML("""<form><p>
+          <input type="radio" name="foo" value="1" />
+        </p></form>""")
+        self.assertEquals("""<form><p>
+          <input type="radio" name="foo" value="1" checked="checked"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': '1'})))
+        self.assertEquals("""<form><p>
+          <input type="radio" name="foo" value="1"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': '2'})))
+
+    def test_fill_input_radio_multi_value(self):
+        html = HTML("""<form><p>
+          <input type="radio" name="foo" value="1" />
+        </p></form>""")
+        self.assertEquals("""<form><p>
+          <input type="radio" name="foo" value="1" checked="checked"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['1']})))
+        self.assertEquals("""<form><p>
+          <input type="radio" name="foo" value="1"/>
+        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['2']})))
+
+    def test_fill_select_no_value_auto(self):
+        html = HTML("""<form><p>
+          <select name="foo">
+            <option>1</option>
+            <option>2</option>
+            <option>3</option>
+          </select>
+        </p></form>""") | HTMLFormFiller()
+        self.assertEquals("""<form><p>
+          <select name="foo">
+            <option>1</option>
+            <option>2</option>
+            <option>3</option>
+          </select>
+        </p></form>""", unicode(html))
+
+    def test_fill_select_no_value_defined(self):
+        html = HTML("""<form><p>
+          <select name="foo">
+            <option value="1">1</option>
+            <option value="2">2</option>
+            <option value="3">3</option>
+          </select>
+        </p></form>""") | HTMLFormFiller()
+        self.assertEquals("""<form><p>
+          <select name="foo">
+            <option value="1">1</option>
+            <option value="2">2</option>
+            <option value="3">3</option>
+          </select>
+        </p></form>""", unicode(html))
+
+    def test_fill_select_single_value_auto(self):
+        html = HTML("""<form><p>
+          <select name="foo">
+            <option>1</option>
+            <option>2</option>
+            <option>3</option>
+          </select>
+        </p></form>""") | HTMLFormFiller(data={'foo': '1'})
+        self.assertEquals("""<form><p>
+          <select name="foo">
+            <option selected="selected">1</option>
+            <option>2</option>
+            <option>3</option>
+          </select>
+        </p></form>""", unicode(html))
+
+    def test_fill_select_single_value_defined(self):
+        html = HTML("""<form><p>
+          <select name="foo">
+            <option value="1">1</option>
+            <option value="2">2</option>
+            <option value="3">3</option>
+          </select>
+        </p></form>""") | HTMLFormFiller(data={'foo': '1'})
+        self.assertEquals("""<form><p>
+          <select name="foo">
+            <option value="1" selected="selected">1</option>
+            <option value="2">2</option>
+            <option value="3">3</option>
+          </select>
+        </p></form>""", unicode(html))
+
+    def test_fill_select_multi_value_auto(self):
+        html = HTML("""<form><p>
+          <select name="foo" multiple>
+            <option>1</option>
+            <option>2</option>
+            <option>3</option>
+          </select>
+        </p></form>""") | HTMLFormFiller(data={'foo': ['1', '3']})
+        self.assertEquals("""<form><p>
+          <select name="foo" multiple="multiple">
+            <option selected="selected">1</option>
+            <option>2</option>
+            <option selected="selected">3</option>
+          </select>
+        </p></form>""", unicode(html))
+
+    def test_fill_select_multi_value_defined(self):
+        html = HTML("""<form><p>
+          <select name="foo" multiple>
+            <option value="1">1</option>
+            <option value="2">2</option>
+            <option value="3">3</option>
+          </select>
+        </p></form>""") | HTMLFormFiller(data={'foo': ['1', '3']})
+        self.assertEquals("""<form><p>
+          <select name="foo" multiple="multiple">
+            <option value="1" selected="selected">1</option>
+            <option value="2">2</option>
+            <option value="3" selected="selected">3</option>
+          </select>
+        </p></form>""", unicode(html))
+
+
+class HTMLSanitizerTestCase(unittest.TestCase):
+
+    def test_sanitize_unchanged(self):
+        html = HTML('<a href="#">fo<br />o</a>')
+        self.assertEquals(u'<a href="#">fo<br/>o</a>',
+                          unicode(html | HTMLSanitizer()))
+
+    def test_sanitize_escape_text(self):
+        html = HTML('<a href="#">fo&amp;</a>')
+        self.assertEquals(u'<a href="#">fo&amp;</a>',
+                          unicode(html | HTMLSanitizer()))
+        html = HTML('<a href="#">&lt;foo&gt;</a>')
+        self.assertEquals(u'<a href="#">&lt;foo&gt;</a>',
+                          unicode(html | HTMLSanitizer()))
+
+    def test_sanitize_entityref_text(self):
+        html = HTML('<a href="#">fo&ouml;</a>')
+        self.assertEquals(u'<a href="#">foƶ</a>',
+                          unicode(html | HTMLSanitizer()))
+
+    def test_sanitize_escape_attr(self):
+        html = HTML('<div title="&lt;foo&gt;"></div>')
+        self.assertEquals(u'<div title="&lt;foo&gt;"/>',
+                          unicode(html | HTMLSanitizer()))
+
+    def test_sanitize_close_empty_tag(self):
+        html = HTML('<a href="#">fo<br>o</a>')
+        self.assertEquals(u'<a href="#">fo<br/>o</a>',
+                          unicode(html | HTMLSanitizer()))
+
+    def test_sanitize_invalid_entity(self):
+        html = HTML('&junk;')
+        self.assertEquals('&amp;junk;', unicode(html | HTMLSanitizer()))
+
+    def test_sanitize_remove_script_elem(self):
+        html = HTML('<script>alert("Foo")</script>')
+        self.assertEquals(u'', unicode(html | HTMLSanitizer()))
+        html = HTML('<SCRIPT SRC="http://example.com/"></SCRIPT>')
+        self.assertEquals(u'', unicode(html | HTMLSanitizer()))
+        self.assertRaises(ParseError, HTML, '<SCR\0IPT>alert("foo")</SCR\0IPT>')
+        self.assertRaises(ParseError, HTML,
+                          '<SCRIPT&XYZ SRC="http://example.com/"></SCRIPT>')
+
+    def test_sanitize_remove_onclick_attr(self):
+        html = HTML('<div onclick=\'alert("foo")\' />')
+        self.assertEquals(u'<div/>', unicode(html | HTMLSanitizer()))
+
+    def test_sanitize_remove_style_scripts(self):
+        sanitizer = HTMLSanitizer(safe_attrs=HTMLSanitizer.SAFE_ATTRS | set(['style']))
+        # Inline style with url() using javascript: scheme
+        html = HTML('<DIV STYLE=\'background: url(javascript:alert("foo"))\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+        # Inline style with url() using javascript: scheme, using control char
+        html = HTML('<DIV STYLE=\'background: url(&#1;javascript:alert("foo"))\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+        # Inline style with url() using javascript: scheme, in quotes
+        html = HTML('<DIV STYLE=\'background: url("javascript:alert(foo)")\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+        # IE expressions in CSS not allowed
+        html = HTML('<DIV STYLE=\'width: expression(alert("foo"));\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+        html = HTML('<DIV STYLE=\'background: url(javascript:alert("foo"));'
+                                 'color: #fff\'>')
+        self.assertEquals(u'<div style="color: #fff"/>',
+                          unicode(html | sanitizer))
+        # Inline style with url() using javascript: scheme, using unicode
+        # escapes
+        html = HTML('<DIV STYLE=\'background: \\75rl(javascript:alert("foo"))\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+        html = HTML('<DIV STYLE=\'background: \\000075rl(javascript:alert("foo"))\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+        html = HTML('<DIV STYLE=\'background: \\75 rl(javascript:alert("foo"))\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+        html = HTML('<DIV STYLE=\'background: \\000075 rl(javascript:alert("foo"))\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+        html = HTML('<DIV STYLE=\'background: \\000075\r\nrl(javascript:alert("foo"))\'>')
+        self.assertEquals(u'<div/>', unicode(html | sanitizer))
+
+    def test_sanitize_remove_src_javascript(self):
+        html = HTML('<img src=\'javascript:alert("foo")\'>')
+        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
+        # Case-insensitive protocol matching
+        html = HTML('<IMG SRC=\'JaVaScRiPt:alert("foo")\'>')
+        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
+        # Grave accents (not parsed)
+        self.assertRaises(ParseError, HTML,
+                          '<IMG SRC=`javascript:alert("RSnake says, \'foo\'")`>')
+        # Protocol encoded using UTF-8 numeric entities
+        html = HTML('<IMG SRC=\'&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;'
+                    '&#112;&#116;&#58;alert("foo")\'>')
+        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
+        # Protocol encoded using UTF-8 numeric entities without a semicolon
+        # (which is allowed because the max number of digits is used)
+        html = HTML('<IMG SRC=\'&#0000106&#0000097&#0000118&#0000097'
+                    '&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116'
+                    '&#0000058alert("foo")\'>')
+        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
+        # Protocol encoded using UTF-8 numeric hex entities without a semicolon
+        # (which is allowed because the max number of digits is used)
+        html = HTML('<IMG SRC=\'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69'
+                    '&#x70&#x74&#x3A;alert("foo")\'>')
+        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
+        # Embedded tab character in protocol
+        html = HTML('<IMG SRC=\'jav\tascript:alert("foo");\'>')
+        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
+        # Embedded tab character in protocol, but encoded this time
+        html = HTML('<IMG SRC=\'jav&#x09;ascript:alert("foo");\'>')
+        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(HTMLFormFiller.__module__))
+    suite.addTest(unittest.makeSuite(HTMLFormFillerTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(HTMLSanitizerTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/filters/tests/i18n.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+import doctest
+from StringIO import StringIO
+import unittest
+
+from genshi.template import MarkupTemplate
+from genshi.filters.i18n import Translator
+
+
+class TranslatorTestCase(unittest.TestCase):
+
+    def test_extract_plural_form(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          ${ngettext("Singular", "Plural", num)}
+        </html>""")
+        translator = Translator()
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(1, len(messages))
+        self.assertEqual((2, 'ngettext', (u'Singular', u'Plural')), messages[0])
+
+    def test_extract_included_attribute_text(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <span title="Foo"></span>
+        </html>""")
+        translator = Translator()
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(1, len(messages))
+        self.assertEqual((2, None, u'Foo'), messages[0])
+
+    def test_extract_attribute_expr(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <input type="submit" value="${_('Save')}" />
+        </html>""")
+        translator = Translator()
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(1, len(messages))
+        self.assertEqual((2, '_', u'Save'), messages[0])
+
+    def test_extract_non_included_attribute_interpolated(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <a href="#anchor_${num}">Foo</a>
+        </html>""")
+        translator = Translator()
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(1, len(messages))
+        self.assertEqual((2, None, u'Foo'), messages[0])
+
+    def test_extract_text_from_sub(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <py:if test="foo">Foo</py:if>
+        </html>""")
+        translator = Translator()
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(1, len(messages))
+        self.assertEqual((2, None, u'Foo'), messages[0])
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TranslatorTestCase, 'test'))
+    suite.addTests(doctest.DocTestSuite(Translator.__module__))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
--- a/genshi/input.py
+++ b/genshi/input.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -11,6 +11,10 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://genshi.edgewall.org/log/.
 
+"""Support for constructing markup streams from files, strings, or other
+sources.
+"""
+
 from itertools import chain
 from xml.parsers import expat
 try:
@@ -22,15 +26,21 @@
 from StringIO import StringIO
 
 from genshi.core import Attrs, QName, Stream, stripentities
-from genshi.core import DOCTYPE, START, END, START_NS, END_NS, TEXT, \
+from genshi.core import START, END, XML_DECL, DOCTYPE, TEXT, START_NS, END_NS, \
                         START_CDATA, END_CDATA, PI, COMMENT
 
 __all__ = ['ET', 'ParseError', 'XMLParser', 'XML', 'HTMLParser', 'HTML']
+__docformat__ = 'restructuredtext en'
 
 def ET(element):
-    """Convert a given ElementTree element to a markup stream."""
+    """Convert a given ElementTree element to a markup stream.
+    
+    :param element: an ElementTree element
+    :return: a markup stream
+    """
     tag_name = QName(element.tag.lstrip('{'))
-    attrs = Attrs(element.items())
+    attrs = Attrs([(QName(attr.lstrip('{')), value)
+                   for attr, value in element.items()])
 
     yield START, (tag_name, attrs), (None, -1, -1)
     if element.text:
@@ -45,12 +55,22 @@
 
 class ParseError(Exception):
     """Exception raised when fatal syntax errors are found in the input being
-    parsed."""
+    parsed.
+    """
 
-    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
+    def __init__(self, message, filename=None, lineno=-1, offset=-1):
+        """Exception initializer.
+        
+        :param message: the error message from the parser
+        :param filename: the path to the file that was parsed
+        :param lineno: the number of the line on which the error was encountered
+        :param offset: the column number where the error was encountered
+        """
+        self.msg = message
+        if filename:
+            message += ', in ' + filename
         Exception.__init__(self, message)
-        self.msg = message
-        self.filename = filename
+        self.filename = filename or '<string>'
         self.lineno = lineno
         self.offset = offset
 
@@ -78,11 +98,12 @@
     def __init__(self, source, filename=None, encoding=None):
         """Initialize the parser for the given XML input.
         
-        @param source: the XML text as a file-like object
-        @param filename: the name of the file, if appropriate
-        @param encoding: the encoding of the file; if not specified, the
-            encoding is assumed to be ASCII, UTF-8, or UTF-16, or whatever the
-            encoding specified in the XML declaration (if any)
+        :param source: the XML text as a file-like object
+        :param filename: the name of the file, if appropriate
+        :param encoding: the encoding of the file; if not specified, the
+                         encoding is assumed to be ASCII, UTF-8, or UTF-16, or
+                         whatever the encoding specified in the XML declaration
+                         (if any)
         """
         self.source = source
         self.filename = filename
@@ -102,6 +123,7 @@
         parser.StartCdataSectionHandler = self._handle_start_cdata
         parser.EndCdataSectionHandler = self._handle_end_cdata
         parser.ProcessingInstructionHandler = self._handle_pi
+        parser.XmlDeclHandler = self._handle_xml_decl
         parser.CommentHandler = self._handle_comment
 
         # Tell Expat that we'll handle non-XML entities ourselves
@@ -119,6 +141,11 @@
         self._queue = []
 
     def parse(self):
+        """Generator that parses the XML source, yielding markup events.
+        
+        :return: a markup event stream
+        :raises ParseError: if the XML text is not well formed
+        """
         def _generate():
             try:
                 bufsize = 4 * 1024 # 4K
@@ -142,8 +169,6 @@
                         break
             except expat.ExpatError, e:
                 msg = str(e)
-                if self.filename:
-                    msg += ', in ' + self.filename
                 raise ParseError(msg, self.filename, e.lineno, e.offset)
         return Stream(_generate()).filter(_coalesce)
 
@@ -182,7 +207,9 @@
                 self.expat.CurrentColumnNumber)
 
     def _handle_start(self, tag, attrib):
-        self._enqueue(START, (QName(tag), Attrs(zip(*[iter(attrib)] * 2))))
+        attrs = Attrs([(QName(name), value) for name, value in
+                       zip(*[iter(attrib)] * 2)])
+        self._enqueue(START, (QName(tag), attrs))
 
     def _handle_end(self, tag):
         self._enqueue(END, QName(tag))
@@ -190,6 +217,9 @@
     def _handle_data(self, text):
         self._enqueue(TEXT, text)
 
+    def _handle_xml_decl(self, version, encoding, standalone):
+        self._enqueue(XML_DECL, (version, encoding, standalone))
+
     def _handle_doctype(self, name, sysid, pubid, has_internal_subset):
         self._enqueue(DOCTYPE, (name, pubid, sysid))
 
@@ -228,6 +258,23 @@
 
 
 def XML(text):
+    """Parse the given XML source and return a markup stream.
+    
+    Unlike with `XMLParser`, the returned stream is reusable, meaning it can be
+    iterated over multiple times:
+    
+    >>> xml = XML('<doc><elem>Foo</elem><elem>Bar</elem></doc>')
+    >>> print xml
+    <doc><elem>Foo</elem><elem>Bar</elem></doc>
+    >>> print xml.select('elem')
+    <elem>Foo</elem><elem>Bar</elem>
+    >>> print xml.select('elem/text()')
+    FooBar
+    
+    :param text: the XML source
+    :return: the parsed XML event stream
+    :raises ParseError: if the XML text is not well-formed
+    """
     return Stream(list(XMLParser(StringIO(text))))
 
 
@@ -256,9 +303,9 @@
     def __init__(self, source, filename=None, encoding='utf-8'):
         """Initialize the parser for the given HTML input.
         
-        @param source: the HTML text as a file-like object
-        @param filename: the name of the file, if known
-        @param filename: encoding of the file; ignored if the input is unicode
+        :param source: the HTML text as a file-like object
+        :param filename: the name of the file, if known
+        :param filename: encoding of the file; ignored if the input is unicode
         """
         html.HTMLParser.__init__(self)
         self.source = source
@@ -268,6 +315,11 @@
         self._open_tags = []
 
     def parse(self):
+        """Generator that parses the HTML source, yielding markup events.
+        
+        :return: a markup event stream
+        :raises ParseError: if the HTML text is not well formed
+        """
         def _generate():
             try:
                 bufsize = 4 * 1024 # 4K
@@ -291,8 +343,6 @@
                         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)
         return Stream(_generate()).filter(_coalesce)
 
@@ -315,7 +365,7 @@
                 value = unicode(name)
             elif not isinstance(value, unicode):
                 value = value.decode(self.encoding, 'replace')
-            fixed_attrib.append((name, stripentities(value)))
+            fixed_attrib.append((QName(name), stripentities(value)))
 
         self._enqueue(START, (QName(tag), Attrs(fixed_attrib)))
         if tag in self._EMPTY_ELEMS:
@@ -337,7 +387,10 @@
         self._enqueue(TEXT, text)
 
     def handle_charref(self, name):
-        text = unichr(int(name))
+        if name.lower().startswith('x'):
+            text = unichr(int(name[1:], 16))
+        else:
+            text = unichr(int(name))
         self._enqueue(TEXT, text)
 
     def handle_entityref(self, name):
@@ -358,6 +411,24 @@
 
 
 def HTML(text, encoding='utf-8'):
+    """Parse the given HTML source and return a markup stream.
+    
+    Unlike with `HTMLParser`, the returned stream is reusable, meaning it can be
+    iterated over multiple times:
+    
+    >>> html = HTML('<body><h1>Foo</h1></body>')
+    >>> print html
+    <body><h1>Foo</h1></body>
+    >>> print html.select('h1')
+    <h1>Foo</h1>
+    >>> print html.select('h1/text()')
+    Foo
+    
+    :param text: the HTML source
+    :return: the parsed XML event stream
+    :raises ParseError: if the HTML text is not well-formed, and error recovery
+                        fails
+    """
     return Stream(list(HTMLParser(StringIO(text), encoding=encoding)))
 
 def _coalesce(stream):
--- a/genshi/output.py
+++ b/genshi/output.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -22,29 +22,116 @@
     from sets import ImmutableSet as frozenset
 import re
 
-from genshi.core import escape, Markup, Namespace, QName, StreamEventKind
-from genshi.core import DOCTYPE, START, END, START_NS, TEXT, START_CDATA, \
-                        END_CDATA, PI, COMMENT, XML_NAMESPACE
+from genshi.core import escape, Attrs, Markup, Namespace, QName, StreamEventKind
+from genshi.core import START, END, TEXT, XML_DECL, DOCTYPE, START_NS, END_NS, \
+                        START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE
 
-__all__ = ['DocType', 'XMLSerializer', 'XHTMLSerializer', 'HTMLSerializer',
-           'TextSerializer']
+__all__ = ['encode', 'get_serializer', 'DocType', 'XMLSerializer',
+           'XHTMLSerializer', 'HTMLSerializer', 'TextSerializer']
+__docformat__ = 'restructuredtext en'
+
+def encode(iterator, method='xml', encoding='utf-8'):
+    """Encode serializer output into a string.
+    
+    :param iterator: the iterator returned from serializing a stream (basically
+                     any iterator that yields unicode objects)
+    :param method: the serialization method; determines how characters not
+                   representable in the specified encoding are treated
+    :param encoding: how the output string should be encoded; if set to `None`,
+                     this method returns a `unicode` object
+    :return: a string or unicode object (depending on the `encoding` parameter)
+    :since: version 0.4.1
+    """
+    output = u''.join(list(iterator))
+    if encoding is not None:
+        errors = 'replace'
+        if method != 'text' and not isinstance(method, TextSerializer):
+            errors = 'xmlcharrefreplace'
+        return output.encode(encoding, errors)
+    return output
+
+def get_serializer(method='xml', **kwargs):
+    """Return a serializer object for the given method.
+    
+    :param method: the serialization method; can be either "xml", "xhtml",
+                   "html", "text", or a custom serializer class
+
+    Any additional keyword arguments are passed to the serializer, and thus
+    depend on the `method` parameter value.
+    
+    :see: `XMLSerializer`, `XHTMLSerializer`, `HTMLSerializer`, `TextSerializer`
+    :since: version 0.4.1
+    """
+    if isinstance(method, basestring):
+        method = {'xml':   XMLSerializer,
+                  'xhtml': XHTMLSerializer,
+                  'html':  HTMLSerializer,
+                  'text':  TextSerializer}[method.lower()]
+    return method(**kwargs)
 
 
 class DocType(object):
     """Defines a number of commonly used DOCTYPE declarations as constants."""
 
-    HTML_STRICT = ('html', '-//W3C//DTD HTML 4.01//EN',
-                   'http://www.w3.org/TR/html4/strict.dtd')
-    HTML_TRANSITIONAL = ('html', '-//W3C//DTD HTML 4.01 Transitional//EN',
-                         'http://www.w3.org/TR/html4/loose.dtd')
+    HTML_STRICT = (
+        'html', '-//W3C//DTD HTML 4.01//EN',
+        'http://www.w3.org/TR/html4/strict.dtd'
+    )
+    HTML_TRANSITIONAL = (
+        'html', '-//W3C//DTD HTML 4.01 Transitional//EN',
+        'http://www.w3.org/TR/html4/loose.dtd'
+    )
+    HTML_FRAMESET = (
+        'html', '-//W3C//DTD HTML 4.01 Frameset//EN',
+        'http://www.w3.org/TR/html4/frameset.dtd'
+    )
     HTML = HTML_STRICT
 
-    XHTML_STRICT = ('html', '-//W3C//DTD XHTML 1.0 Strict//EN',
-                    'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd')
-    XHTML_TRANSITIONAL = ('html', '-//W3C//DTD XHTML 1.0 Transitional//EN',
-                          'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd')
+    HTML5 = ('html', None, None)
+
+    XHTML_STRICT = (
+        'html', '-//W3C//DTD XHTML 1.0 Strict//EN',
+        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
+    )
+    XHTML_TRANSITIONAL = (
+        'html', '-//W3C//DTD XHTML 1.0 Transitional//EN',
+        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
+    )
+    XHTML_FRAMESET = (
+        'html', '-//W3C//DTD XHTML 1.0 Frameset//EN',
+        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd'
+    )
     XHTML = XHTML_STRICT
 
+    def get(cls, name):
+        """Return the ``(name, pubid, sysid)`` tuple of the ``DOCTYPE``
+        declaration for the specified name.
+        
+        The following names are recognized in this version:
+         * "html" or "html-strict" for the HTML 4.01 strict DTD
+         * "html-transitional" for the HTML 4.01 transitional DTD
+         * "html-transitional" for the HTML 4.01 frameset DTD
+         * "html5" for the ``DOCTYPE`` proposed for HTML5
+         * "xhtml" or "xhtml-strict" for the XHTML 1.0 strict DTD
+         * "xhtml-transitional" for the XHTML 1.0 transitional DTD
+         * "xhtml-frameset" for the XHTML 1.0 frameset DTD
+        
+        :param name: the name of the ``DOCTYPE``
+        :return: the ``(name, pubid, sysid)`` tuple for the requested
+                 ``DOCTYPE``, or ``None`` if the name is not recognized
+        :since: version 0.4.1
+        """
+        return {
+            'html': cls.HTML, 'html-strict': cls.HTML_STRICT,
+            'html-transitional': DocType.HTML_TRANSITIONAL,
+            'html-frameset': DocType.HTML_FRAMESET,
+            'html5': cls.HTML5,
+            'xhtml': cls.XHTML, 'xhtml-strict': cls.XHTML_STRICT,
+            'xhtml-transitional': cls.XHTML_TRANSITIONAL,
+            'xhtml-frameset': cls.XHTML_FRAMESET,
+        }.get(name.lower())
+    get = classmethod(get)
+
 
 class XMLSerializer(object):
     """Produces XML text from an event stream.
@@ -57,25 +144,127 @@
 
     _PRESERVE_SPACE = frozenset()
 
-    def __init__(self, doctype=None, strip_whitespace=True):
+    def __init__(self, doctype=None, strip_whitespace=True,
+                 namespace_prefixes=None):
         """Initialize the XML serializer.
         
-        @param doctype: a `(name, pubid, sysid)` tuple that represents the
-            DOCTYPE declaration that should be included at the top of the
-            generated output
-        @param strip_whitespace: whether extraneous whitespace should be
-            stripped from the output
+        :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
+                        DOCTYPE declaration that should be included at the top
+                        of the generated output, or the name of a DOCTYPE as
+                        defined in `DocType.get`
+        :param strip_whitespace: whether extraneous whitespace should be
+                                 stripped from the output
+        :note: Changed in 0.4.2: The  `doctype` parameter can now be a string.
         """
         self.preamble = []
         if doctype:
+            if isinstance(doctype, basestring):
+                doctype = DocType.get(doctype)
             self.preamble.append((DOCTYPE, doctype, (None, -1, -1)))
         self.filters = [EmptyTagFilter()]
         if strip_whitespace:
             self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
+        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
 
     def __call__(self, stream):
-        ns_attrib = []
-        ns_mapping = {XML_NAMESPACE.uri: 'xml'}
+        have_decl = have_doctype = False
+        in_cdata = False
+
+        stream = chain(self.preamble, stream)
+        for filter_ in self.filters:
+            stream = filter_(stream)
+        for kind, data, pos in stream:
+
+            if kind is START or kind is EMPTY:
+                tag, attrib = data
+                buf = ['<', tag]
+                for attr, value in attrib:
+                    buf += [' ', attr, '="', escape(value), '"']
+                buf.append(kind is EMPTY and '/>' or '>')
+                yield Markup(u''.join(buf))
+
+            elif kind is END:
+                yield Markup('</%s>' % data)
+
+            elif kind is TEXT:
+                if in_cdata:
+                    yield data
+                else:
+                    yield escape(data, quotes=False)
+
+            elif kind is COMMENT:
+                yield Markup('<!--%s-->' % data)
+
+            elif kind is XML_DECL and not have_decl:
+                version, encoding, standalone = data
+                buf = ['<?xml version="%s"' % version]
+                if encoding:
+                    buf.append(' encoding="%s"' % encoding)
+                if standalone != -1:
+                    standalone = standalone and 'yes' or 'no'
+                    buf.append(' standalone="%s"' % standalone)
+                buf.append('?>\n')
+                yield Markup(u''.join(buf))
+                have_decl = True
+
+            elif kind is DOCTYPE and not have_doctype:
+                name, pubid, sysid = data
+                buf = ['<!DOCTYPE %s']
+                if pubid:
+                    buf.append(' PUBLIC "%s"')
+                elif sysid:
+                    buf.append(' SYSTEM')
+                if sysid:
+                    buf.append(' "%s"')
+                buf.append('>\n')
+                yield Markup(u''.join(buf), *filter(None, data))
+                have_doctype = True
+
+            elif kind is START_CDATA:
+                yield Markup('<![CDATA[')
+                in_cdata = True
+
+            elif kind is END_CDATA:
+                yield Markup(']]>')
+                in_cdata = False
+
+            elif kind is PI:
+                yield Markup('<?%s %s?>' % data)
+
+
+class XHTMLSerializer(XMLSerializer):
+    """Produces XHTML text from an event stream.
+    
+    >>> from genshi.builder import tag
+    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
+    >>> print ''.join(XHTMLSerializer()(elem.generate()))
+    <div><a href="foo"></a><br /><hr noshade="noshade" /></div>
+    """
+
+    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
+                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
+                              'param'])
+    _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
+                                'defer', 'disabled', 'ismap', 'multiple',
+                                'nohref', 'noresize', 'noshade', 'nowrap'])
+    _PRESERVE_SPACE = frozenset([
+        QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'),
+        QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea')
+    ])
+
+    def __init__(self, doctype=None, strip_whitespace=True,
+                 namespace_prefixes=None):
+        super(XHTMLSerializer, self).__init__(doctype, False)
+        self.filters = [EmptyTagFilter()]
+        if strip_whitespace:
+            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
+        namespace_prefixes = namespace_prefixes or {}
+        namespace_prefixes['http://www.w3.org/1999/xhtml'] = ''
+        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
+
+    def __call__(self, stream):
+        boolean_attrs = self._BOOLEAN_ATTRS
+        empty_elems = self._EMPTY_ELEMS
         have_doctype = False
         in_cdata = False
 
@@ -86,42 +275,22 @@
 
             if kind is START or kind is EMPTY:
                 tag, attrib = data
-
-                tagname = tag.localname
-                namespace = tag.namespace
-                if namespace:
-                    if namespace in ns_mapping:
-                        prefix = ns_mapping[namespace]
-                        if prefix:
-                            tagname = '%s:%s' % (prefix, tagname)
+                buf = ['<', tag]
+                for attr, value in attrib:
+                    if attr in boolean_attrs:
+                        value = attr
+                    buf += [' ', attr, '="', escape(value), '"']
+                if kind is EMPTY:
+                    if tag in empty_elems:
+                        buf.append(' />')
                     else:
-                        ns_attrib.append((QName('xmlns'), namespace))
-                buf = ['<', tagname]
-
-                if ns_attrib:
-                    attrib += tuple(ns_attrib)
-                for attr, value in attrib:
-                    attrname = attr.localname
-                    attrns = attr.namespace
-                    if attrns:
-                        prefix = ns_mapping.get(attrns)
-                        if prefix:
-                            attrname = '%s:%s' % (prefix, attrname)
-                    buf += [' ', attrname, '="', escape(value), '"']
-                ns_attrib = []
-
-                buf.append(kind is EMPTY and '/>' or '>')
-
+                        buf.append('></%s>' % tag)
+                else:
+                    buf.append('>')
                 yield Markup(u''.join(buf))
 
             elif kind is END:
-                tag = data
-                tagname = tag.localname
-                if tag.namespace:
-                    prefix = ns_mapping.get(tag.namespace)
-                    if prefix:
-                        tagname = '%s:%s' % (prefix, tag.localname)
-                yield Markup('</%s>' % tagname)
+                yield Markup('</%s>' % data)
 
             elif kind is TEXT:
                 if in_cdata:
@@ -145,144 +314,6 @@
                 yield Markup(u''.join(buf), *filter(None, data))
                 have_doctype = True
 
-            elif kind is START_NS:
-                prefix, uri = data
-                if uri not in ns_mapping:
-                    ns_mapping[uri] = prefix
-                    if not prefix:
-                        ns_attrib.append((QName('xmlns'), uri))
-                    else:
-                        ns_attrib.append((QName('xmlns:%s' % prefix), uri))
-
-            elif kind is START_CDATA:
-                yield Markup('<![CDATA[')
-                in_cdata = True
-
-            elif kind is END_CDATA:
-                yield Markup(']]>')
-                in_cdata = False
-
-            elif kind is PI:
-                yield Markup('<?%s %s?>' % data)
-
-
-class XHTMLSerializer(XMLSerializer):
-    """Produces XHTML text from an event stream.
-    
-    >>> from genshi.builder import tag
-    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
-    >>> print ''.join(XHTMLSerializer()(elem.generate()))
-    <div><a href="foo"></a><br /><hr noshade="noshade" /></div>
-    """
-
-    NAMESPACE = Namespace('http://www.w3.org/1999/xhtml')
-
-    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
-                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
-                              'param'])
-    _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
-                                'defer', 'disabled', 'ismap', 'multiple',
-                                'nohref', 'noresize', 'noshade', 'nowrap'])
-    _PRESERVE_SPACE = frozenset([
-        QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'),
-        QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea')
-    ])
-
-    def __call__(self, stream):
-        namespace = self.NAMESPACE
-        ns_attrib = []
-        ns_mapping = {XML_NAMESPACE.uri: 'xml'}
-        boolean_attrs = self._BOOLEAN_ATTRS
-        empty_elems = self._EMPTY_ELEMS
-        have_doctype = False
-        in_cdata = False
-
-        stream = chain(self.preamble, stream)
-        for filter_ in self.filters:
-            stream = filter_(stream)
-        for kind, data, pos in stream:
-
-            if kind is START or kind is EMPTY:
-                tag, attrib = data
-
-                tagname = tag.localname
-                tagns = tag.namespace
-                if tagns:
-                    if tagns in ns_mapping:
-                        prefix = ns_mapping[tagns]
-                        if prefix:
-                            tagname = '%s:%s' % (prefix, tagname)
-                    else:
-                        ns_attrib.append((QName('xmlns'), tagns))
-                buf = ['<', tagname]
-
-                if ns_attrib:
-                    attrib += tuple(ns_attrib)
-                for attr, value in attrib:
-                    attrname = attr.localname
-                    attrns = attr.namespace
-                    if attrns:
-                        prefix = ns_mapping.get(attrns)
-                        if prefix:
-                            attrname = '%s:%s' % (prefix, attrname)
-                    if attrname in boolean_attrs:
-                        if value:
-                            buf += [' ', attrname, '="', attrname, '"']
-                    else:
-                        buf += [' ', attrname, '="', escape(value), '"']
-                ns_attrib = []
-
-                if kind is EMPTY:
-                    if (tagns and tagns != namespace.uri) \
-                            or tagname in empty_elems:
-                        buf.append(' />')
-                    else:
-                        buf.append('></%s>' % tagname)
-                else:
-                    buf.append('>')
-
-                yield Markup(u''.join(buf))
-
-            elif kind is END:
-                tag = data
-                tagname = tag.localname
-                if tag.namespace:
-                    prefix = ns_mapping.get(tag.namespace)
-                    if prefix:
-                        tagname = '%s:%s' % (prefix, tagname)
-                yield Markup('</%s>' % tagname)
-
-            elif kind is TEXT:
-                if in_cdata:
-                    yield data
-                else:
-                    yield escape(data, quotes=False)
-
-            elif kind is COMMENT:
-                yield Markup('<!--%s-->' % data)
-
-            elif kind is DOCTYPE and not have_doctype:
-                name, pubid, sysid = data
-                buf = ['<!DOCTYPE %s']
-                if pubid:
-                    buf.append(' PUBLIC "%s"')
-                elif sysid:
-                    buf.append(' SYSTEM')
-                if sysid:
-                    buf.append(' "%s"')
-                buf.append('>\n')
-                yield Markup(u''.join(buf), *filter(None, data))
-                have_doctype = True
-
-            elif kind is START_NS:
-                prefix, uri = data
-                if uri not in ns_mapping:
-                    ns_mapping[uri] = prefix
-                    if not prefix:
-                        ns_attrib.append((QName('xmlns'), uri))
-                    else:
-                        ns_attrib.append((QName('xmlns:%s' % prefix), uri))
-
             elif kind is START_CDATA:
                 yield Markup('<![CDATA[')
                 in_cdata = True
@@ -304,28 +335,28 @@
     <div><a href="foo"></a><br><hr noshade></div>
     """
 
-    _NOESCAPE_ELEMS = frozenset([QName('script'),
-                                 QName('http://www.w3.org/1999/xhtml}script'),
-                                 QName('style'),
-                                 QName('http://www.w3.org/1999/xhtml}style')])
+    _NOESCAPE_ELEMS = frozenset([
+        QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
+        QName('style'), QName('http://www.w3.org/1999/xhtml}style')
+    ])
 
     def __init__(self, doctype=None, strip_whitespace=True):
         """Initialize the HTML serializer.
         
-        @param doctype: a `(name, pubid, sysid)` tuple that represents the
-            DOCTYPE declaration that should be included at the top of the
-            generated output
-        @param strip_whitespace: whether extraneous whitespace should be
-            stripped from the output
+        :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
+                        DOCTYPE declaration that should be included at the top
+                        of the generated output
+        :param strip_whitespace: whether extraneous whitespace should be
+                                 stripped from the output
         """
         super(HTMLSerializer, self).__init__(doctype, False)
+        self.filters = [EmptyTagFilter()]
         if strip_whitespace:
             self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
                                                  self._NOESCAPE_ELEMS))
+        self.filters.append(NamespaceStripper('http://www.w3.org/1999/xhtml'))
 
     def __call__(self, stream):
-        namespace = self.NAMESPACE
-        ns_mapping = {}
         boolean_attrs = self._BOOLEAN_ATTRS
         empty_elems = self._EMPTY_ELEMS
         noescape_elems = self._NOESCAPE_ELEMS
@@ -339,35 +370,23 @@
 
             if kind is START or kind is EMPTY:
                 tag, attrib = data
-                if not tag.namespace or tag in namespace:
-                    tagname = tag.localname
-                    buf = ['<', tagname]
-
-                    for attr, value in attrib:
-                        attrname = attr.localname
-                        if not attr.namespace or attr in namespace:
-                            if attrname in boolean_attrs:
-                                if value:
-                                    buf += [' ', attrname]
-                            else:
-                                buf += [' ', attrname, '="', escape(value), '"']
-
-                    buf.append('>')
-
-                    if kind is EMPTY:
-                        if tagname not in empty_elems:
-                            buf.append('</%s>' % tagname)
-
-                    yield Markup(u''.join(buf))
-
-                    if tagname in noescape_elems:
-                        noescape = True
+                buf = ['<', tag]
+                for attr, value in attrib:
+                    if attr in boolean_attrs:
+                        if value:
+                            buf += [' ', attr]
+                    else:
+                        buf += [' ', attr, '="', escape(value), '"']
+                buf.append('>')
+                if kind is EMPTY:
+                    if tag not in empty_elems:
+                        buf.append('</%s>' % tag)
+                yield Markup(u''.join(buf))
+                if tag in noescape_elems:
+                    noescape = True
 
             elif kind is END:
-                tag = data
-                if not tag.namespace or tag in namespace:
-                    yield Markup('</%s>' % tag.localname)
-
+                yield Markup('</%s>' % data)
                 noescape = False
 
             elif kind is TEXT:
@@ -392,9 +411,6 @@
                 yield Markup(u''.join(buf), *filter(None, data))
                 have_doctype = True
 
-            elif kind is START_NS and data[1] not in ns_mapping:
-                ns_mapping[data[1]] = data[0]
-
             elif kind is PI:
                 yield Markup('<?%s %s?>' % data)
 
@@ -423,8 +439,9 @@
     """
 
     def __call__(self, stream):
-        for kind, data, pos in stream:
-            if kind is TEXT:
+        for event in stream:
+            if event[0] is TEXT:
+                data = event[1]
                 if type(data) is Markup:
                     data = data.striptags().stripentities()
                 yield unicode(data)
@@ -439,36 +456,206 @@
 
     def __call__(self, stream):
         prev = (None, None, None)
-        for kind, data, pos in stream:
+        for ev in stream:
             if prev[0] is START:
-                if kind is END:
+                if ev[0] is END:
                     prev = EMPTY, prev[1], prev[2]
                     yield prev
                     continue
                 else:
                     yield prev
-            if kind is not START:
-                yield kind, data, pos
-            prev = kind, data, pos
+            if ev[0] is not START:
+                yield ev
+            prev = ev
 
 
 EMPTY = EmptyTagFilter.EMPTY
 
 
+class NamespaceFlattener(object):
+    r"""Output stream filter that removes namespace information from the stream,
+    instead adding namespace attributes and prefixes as needed.
+    
+    :param prefixes: optional mapping of namespace URIs to prefixes
+    
+    >>> from genshi.input import XML
+    >>> xml = XML('''<doc xmlns="NS1" xmlns:two="NS2">
+    ...   <two:item/>
+    ... </doc>''')
+    >>> for kind, data, pos in NamespaceFlattener()(xml):
+    ...     print kind, repr(data)
+    START (u'doc', Attrs([(u'xmlns', u'NS1'), (u'xmlns:two', u'NS2')]))
+    TEXT u'\n  '
+    START (u'two:item', Attrs())
+    END u'two:item'
+    TEXT u'\n'
+    END u'doc'
+    """
+
+    def __init__(self, prefixes=None):
+        self.prefixes = {XML_NAMESPACE.uri: 'xml'}
+        if prefixes is not None:
+            self.prefixes.update(prefixes)
+
+    def __call__(self, stream):
+        prefixes = dict([(v, [k]) for k, v in self.prefixes.items()])
+        namespaces = {XML_NAMESPACE.uri: ['xml']}
+        def _push_ns(prefix, uri):
+            namespaces.setdefault(uri, []).append(prefix)
+            prefixes.setdefault(prefix, []).append(uri)
+
+        ns_attrs = []
+        _push_ns_attr = ns_attrs.append
+        def _make_ns_attr(prefix, uri):
+            return u'xmlns%s' % (prefix and ':%s' % prefix or ''), uri
+
+        def _gen_prefix():
+            val = 0
+            while 1:
+                val += 1
+                yield 'ns%d' % val
+        _gen_prefix = _gen_prefix().next
+
+        for kind, data, pos in stream:
+
+            if kind is START or kind is EMPTY:
+                tag, attrs = data
+
+                tagname = tag.localname
+                tagns = tag.namespace
+                if tagns:
+                    if tagns in namespaces:
+                        prefix = namespaces[tagns][-1]
+                        if prefix:
+                            tagname = u'%s:%s' % (prefix, tagname)
+                    else:
+                        _push_ns_attr((u'xmlns', tagns))
+                        _push_ns('', tagns)
+
+                new_attrs = []
+                for attr, value in attrs:
+                    attrname = attr.localname
+                    attrns = attr.namespace
+                    if attrns:
+                        if attrns not in namespaces:
+                            prefix = _gen_prefix()
+                            _push_ns(prefix, attrns)
+                            _push_ns_attr(('xmlns:%s' % prefix, attrns))
+                        else:
+                            prefix = namespaces[attrns][-1]
+                        if prefix:
+                            attrname = u'%s:%s' % (prefix, attrname)
+                    new_attrs.append((attrname, value))
+
+                yield kind, (tagname, Attrs(ns_attrs + new_attrs)), pos
+                del ns_attrs[:]
+
+            elif kind is END:
+                tagname = data.localname
+                tagns = data.namespace
+                if tagns:
+                    prefix = namespaces[tagns][-1]
+                    if prefix:
+                        tagname = u'%s:%s' % (prefix, tagname)
+                yield kind, tagname, pos
+
+            elif kind is START_NS:
+                prefix, uri = data
+                if uri not in namespaces:
+                    prefix = prefixes.get(uri, [prefix])[-1]
+                    _push_ns_attr(_make_ns_attr(prefix, uri))
+                _push_ns(prefix, uri)
+
+            elif kind is END_NS:
+                if data in prefixes:
+                    uris = prefixes.get(data)
+                    uri = uris.pop()
+                    if not uris:
+                        del prefixes[data]
+                    if uri not in uris or uri != uris[-1]:
+                        uri_prefixes = namespaces[uri]
+                        uri_prefixes.pop()
+                        if not uri_prefixes:
+                            del namespaces[uri]
+                    if ns_attrs:
+                        attr = _make_ns_attr(data, uri)
+                        if attr in ns_attrs:
+                            ns_attrs.remove(attr)
+
+            else:
+                yield kind, data, pos
+
+
+class NamespaceStripper(object):
+    r"""Stream filter that removes all namespace information from a stream, and
+    optionally strips out all tags not in a given namespace.
+    
+    :param namespace: the URI of the namespace that should not be stripped. If
+                      not set, only elements with no namespace are included in
+                      the output.
+    
+    >>> from genshi.input import XML
+    >>> xml = XML('''<doc xmlns="NS1" xmlns:two="NS2">
+    ...   <two:item/>
+    ... </doc>''')
+    >>> for kind, data, pos in NamespaceStripper(Namespace('NS1'))(xml):
+    ...     print kind, repr(data)
+    START (u'doc', Attrs())
+    TEXT u'\n  '
+    TEXT u'\n'
+    END u'doc'
+    """
+
+    def __init__(self, namespace=None):
+        if namespace is not None:
+            self.namespace = Namespace(namespace)
+        else:
+            self.namespace = {}
+
+    def __call__(self, stream):
+        namespace = self.namespace
+
+        for kind, data, pos in stream:
+
+            if kind is START or kind is EMPTY:
+                tag, attrs = data
+                if tag.namespace and tag not in namespace:
+                    continue
+
+                new_attrs = []
+                for attr, value in attrs:
+                    if not attr.namespace or attr in namespace:
+                        new_attrs.append((attr, value))
+
+                data = tag.localname, Attrs(new_attrs)
+
+            elif kind is END:
+                if data.namespace and data not in namespace:
+                    continue
+                data = data.localname
+
+            elif kind is START_NS or kind is END_NS:
+                continue
+
+            yield kind, data, pos
+
+
 class WhitespaceFilter(object):
     """A filter that removes extraneous ignorable white space from the
-    stream."""
+    stream.
+    """
 
     def __init__(self, preserve=None, noescape=None):
         """Initialize the filter.
         
-        @param preserve: a set or sequence of tag names for which white-space
-            should be preserved
-        @param noescape: a set or sequence of tag names for which text content
-            should not be escaped
+        :param preserve: a set or sequence of tag names for which white-space
+                         should be preserved
+        :param noescape: a set or sequence of tag names for which text content
+                         should not be escaped
         
         The `noescape` set is expected to refer to elements that cannot contain
-        further child elements (such as <style> or <script> in HTML documents).
+        further child elements (such as ``<style>`` or ``<script>`` in HTML
+        documents).
         """
         if preserve is None:
             preserve = []
@@ -490,6 +677,7 @@
         push_text = textbuf.append
         pop_text = textbuf.pop
         for kind, data, pos in chain(stream, [(None, None, None)]):
+
             if kind is TEXT:
                 if noescape:
                     data = Markup(data)
--- a/genshi/path.py
+++ b/genshi/path.py
@@ -38,6 +38,7 @@
 from genshi.core import START, END, TEXT, COMMENT, PI
 
 __all__ = ['Path', 'PathSyntaxError']
+__docformat__ = 'restructuredtext en'
 
 
 class Axis(object):
@@ -75,7 +76,10 @@
     def __init__(self, text, filename=None, lineno=-1):
         """Create the path object from a string.
         
-        @param text: the path expression
+        :param text: the path expression
+        :param filename: the name of the file in which the path expression was
+                         found (used in error messages)
+        :param lineno: the line on which the expression was found
         """
         self.source = text
         self.paths = PathParser(text, filename, lineno).parse()
@@ -105,10 +109,11 @@
         >>> print Path('.//child/text()').select(xml)
         Text
         
-        @param stream: the stream to select from
-        @param namespaces: (optional) a mapping of namespace prefixes to URIs
-        @param variables: (optional) a mapping of variable names to values
-        @return: the substream matching the path, or an empty stream
+        :param stream: the stream to select from
+        :param namespaces: (optional) a mapping of namespace prefixes to URIs
+        :param variables: (optional) a mapping of variable names to values
+        :return: the substream matching the path, or an empty stream
+        :rtype: `Stream`
         """
         if namespaces is None:
             namespaces = {}
@@ -140,16 +145,16 @@
         """Returns a function that can be used to track whether the path matches
         a specific stream event.
         
-        The function returned expects the positional arguments `event`,
-        `namespaces` and `variables`. The first is a stream event, while the
+        The function returned expects the positional arguments ``event``,
+        ``namespaces`` and ``variables``. The first is a stream event, while the
         latter two are a mapping of namespace prefixes to URIs, and a mapping
         of variable names to values, respectively. In addition, the function
-        accepts an `updateonly` keyword argument that default to `False`. If
-        it is set to `True`, the function only updates its internal state,
+        accepts an ``updateonly`` keyword argument that default to ``False``. If
+        it is set to ``True``, the function only updates its internal state,
         but does not perform any tests or return a result.
         
         If the path matches the event, the function returns the match (for
-        example, a `START` or `TEXT` event.) Otherwise, it returns `None`.
+        example, a `START` or `TEXT` event.) Otherwise, it returns ``None``.
         
         >>> from genshi.input import XML
         >>> xml = XML('<root><elem><child id="1"/></elem><child id="2"/></root>')
@@ -158,6 +163,13 @@
         ...     if test(event, {}, {}):
         ...         print event[0], repr(event[1])
         START (QName(u'child'), Attrs([(QName(u'id'), u'2')]))
+        
+        :param ignore_context: if `True`, the path is interpreted like a pattern
+                               in XSLT, meaning for example that it will match
+                               at any depth
+        :return: a function that can be used to test individual events in a
+                 stream against the path
+        :rtype: ``function``
         """
         paths = [(p, len(p), [0], [], [0] * len(p)) for p in [
             (ignore_context and [_DOTSLASHSLASH] or []) + p for p in self.paths
--- a/genshi/template/__init__.py
+++ b/genshi/template/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -13,9 +13,11 @@
 
 """Implementation of the template engine."""
 
-from genshi.template.core import Context, Template, TemplateError, \
+from genshi.template.base import Context, Template, TemplateError, \
                                  TemplateRuntimeError, TemplateSyntaxError, \
                                  BadDirectiveError
 from genshi.template.loader import TemplateLoader, TemplateNotFound
 from genshi.template.markup import MarkupTemplate
 from genshi.template.text import TextTemplate
+
+__docformat__ = 'restructuredtext en'
new file mode 100644
--- /dev/null
+++ b/genshi/template/base.py
@@ -0,0 +1,493 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2007 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+"""Basic templating functionality."""
+
+try:
+    from collections import deque
+except ImportError:
+    class deque(list):
+        def appendleft(self, x): self.insert(0, x)
+        def popleft(self): return self.pop(0)
+import os
+from StringIO import StringIO
+
+from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
+from genshi.input import ParseError
+
+__all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError',
+           'TemplateSyntaxError', 'BadDirectiveError']
+__docformat__ = 'restructuredtext en'
+
+
+class TemplateError(Exception):
+    """Base exception class for errors related to template processing."""
+
+    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
+        """Create the exception.
+        
+        :param message: the error message
+        :param filename: the filename of the template
+        :param lineno: the number of line in the template at which the error
+                       occurred
+        :param offset: the column number at which the error occurred
+        """
+        self.msg = message #: the error message string
+        if filename != '<string>' or lineno >= 0:
+            message = '%s (%s, line %d)' % (self.msg, filename, lineno)
+        Exception.__init__(self, message)
+        self.filename = filename #: the name of the template file
+        self.lineno = lineno #: the number of the line containing the error
+        self.offset = offset #: the offset on the line
+
+
+class TemplateSyntaxError(TemplateError):
+    """Exception raised when an expression in a template causes a Python syntax
+    error, or the template is not well-formed.
+    """
+
+    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
+        """Create the exception
+        
+        :param message: the error message
+        :param filename: the filename of the template
+        :param lineno: the number of line in the template at which the error
+                       occurred
+        :param offset: the column number at which the error occurred
+        """
+        if isinstance(message, SyntaxError) and message.lineno is not None:
+            message = str(message).replace(' (line %d)' % message.lineno, '')
+        TemplateError.__init__(self, message, filename, lineno)
+
+
+class BadDirectiveError(TemplateSyntaxError):
+    """Exception raised when an unknown directive is encountered when parsing
+    a template.
+    
+    An unknown directive is any attribute using the namespace for directives,
+    with a local name that doesn't match any registered directive.
+    """
+
+    def __init__(self, name, filename='<string>', lineno=-1):
+        """Create the exception
+        
+        :param name: the name of the directive
+        :param filename: the filename of the template
+        :param lineno: the number of line in the template at which the error
+                       occurred
+        """
+        TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name,
+                                     filename, lineno)
+
+
+class TemplateRuntimeError(TemplateError):
+    """Exception raised when an the evaluation of a Python expression in a
+    template causes an error.
+    """
+
+
+class Context(object):
+    """Container for template input data.
+    
+    A context provides a stack of scopes (represented by dictionaries).
+    
+    Template directives such as loops can push a new scope on the stack with
+    data that should only be available inside the loop. When the loop
+    terminates, that scope can get popped off the stack again.
+    
+    >>> ctxt = Context(one='foo', other=1)
+    >>> ctxt.get('one')
+    'foo'
+    >>> ctxt.get('other')
+    1
+    >>> ctxt.push(dict(one='frost'))
+    >>> ctxt.get('one')
+    'frost'
+    >>> ctxt.get('other')
+    1
+    >>> ctxt.pop()
+    {'one': 'frost'}
+    >>> ctxt.get('one')
+    'foo'
+    """
+
+    def __init__(self, **data):
+        """Initialize the template context with the given keyword arguments as
+        data.
+        """
+        self.frames = deque([data])
+        self.pop = self.frames.popleft
+        self.push = self.frames.appendleft
+        self._match_templates = []
+
+        # Helper functions for use in expressions
+        def defined(name):
+            """Return whether a variable with the specified name exists in the
+            expression scope."""
+            return name in self
+        def value_of(name, default=None):
+            """If a variable of the specified name is defined, return its value.
+            Otherwise, return the provided default value, or ``None``."""
+            return self.get(name, default)
+        data.setdefault('defined', defined)
+        data.setdefault('value_of', value_of)
+
+    def __repr__(self):
+        return repr(list(self.frames))
+
+    def __contains__(self, key):
+        """Return whether a variable exists in any of the scopes.
+        
+        :param key: the name of the variable
+        """
+        return self._find(key)[1] is not None
+
+    def __delitem__(self, key):
+        """Remove a variable from all scopes.
+        
+        :param key: the name of the variable
+        """
+        for frame in self.frames:
+            if key in frame:
+                del frame[key]
+
+    def __getitem__(self, key):
+        """Get a variables's value, starting at the current scope and going
+        upward.
+        
+        :param key: the name of the variable
+        :return: the variable value
+        :raises KeyError: if the requested variable wasn't found in any scope
+        """
+        value, frame = self._find(key)
+        if frame is None:
+            raise KeyError(key)
+        return value
+
+    def __len__(self):
+        """Return the number of distinctly named variables in the context.
+        
+        :return: the number of variables in the context
+        """
+        return len(self.items())
+
+    def __setitem__(self, key, value):
+        """Set a variable in the current scope.
+        
+        :param key: the name of the variable
+        :param value: the variable value
+        """
+        self.frames[0][key] = value
+
+    def _find(self, key, default=None):
+        """Retrieve a given variable's value and the frame it was found in.
+
+        Intended primarily for internal use by directives.
+        
+        :param key: the name of the variable
+        :param default: the default value to return when the variable is not
+                        found
+        """
+        for frame in self.frames:
+            if key in frame:
+                return frame[key], frame
+        return default, None
+
+    def get(self, key, default=None):
+        """Get a variable's value, starting at the current scope and going
+        upward.
+        
+        :param key: the name of the variable
+        :param default: the default value to return when the variable is not
+                        found
+        """
+        for frame in self.frames:
+            if key in frame:
+                return frame[key]
+        return default
+
+    def keys(self):
+        """Return the name of all variables in the context.
+        
+        :return: a list of variable names
+        """
+        keys = []
+        for frame in self.frames:
+            keys += [key for key in frame if key not in keys]
+        return keys
+
+    def items(self):
+        """Return a list of ``(name, value)`` tuples for all variables in the
+        context.
+        
+        :return: a list of variables
+        """
+        return [(key, self.get(key)) for key in self.keys()]
+
+    def push(self, data):
+        """Push a new scope on the stack.
+        
+        :param data: the data dictionary to push on the context stack.
+        """
+
+    def pop(self):
+        """Pop the top-most scope from the stack."""
+
+
+def _apply_directives(stream, ctxt, directives):
+    """Apply the given directives to the stream.
+    
+    :param stream: the stream the directives should be applied to
+    :param ctxt: the `Context`
+    :param directives: the list of directives to apply
+    :return: the stream with the given directives applied
+    """
+    if directives:
+        stream = directives[0](iter(stream), ctxt, directives[1:])
+    return stream
+
+
+class TemplateMeta(type):
+    """Meta class for templates."""
+
+    def __new__(cls, name, bases, d):
+        if 'directives' in d:
+            d['_dir_by_name'] = dict(d['directives'])
+            d['_dir_order'] = [directive[1] for directive in d['directives']]
+
+        return type.__new__(cls, name, bases, d)
+
+
+class Template(object):
+    """Abstract template base class.
+    
+    This class implements most of the template processing model, but does not
+    specify the syntax of templates.
+    """
+    __metaclass__ = TemplateMeta
+
+    EXPR = StreamEventKind('EXPR')
+    """Stream event kind representing a Python expression."""
+
+    INCLUDE = StreamEventKind('INCLUDE')
+    """Stream event kind representing the inclusion of another template."""
+
+    SUB = StreamEventKind('SUB')
+    """Stream event kind representing a nested stream to which one or more
+    directives should be applied.
+    """
+
+    def __init__(self, source, basedir=None, filename=None, loader=None,
+                 encoding=None, lookup='lenient'):
+        """Initialize a template from either a string, a file-like object, or
+        an already parsed markup stream.
+        
+        :param source: a string, file-like object, or markup stream to read the
+                       template from
+        :param basedir: the base directory containing the template file; when
+                        loaded from a `TemplateLoader`, this will be the
+                        directory on the template search path in which the
+                        template was found
+        :param filename: the name of the template file, relative to the given
+                         base directory
+        :param loader: the `TemplateLoader` to use for loading included
+                       templates
+        :param encoding: the encoding of the `source`
+        :param lookup: the variable lookup mechanism; either "lenient" (the
+                       default), "strict", or a custom lookup class
+        """
+        self.basedir = basedir
+        self.filename = filename
+        if basedir and filename:
+            self.filepath = os.path.join(basedir, filename)
+        else:
+            self.filepath = filename
+        self.loader = loader
+        self.lookup = lookup
+
+        if isinstance(source, basestring):
+            source = StringIO(source)
+        else:
+            source = source
+        try:
+            self.stream = list(self._prepare(self._parse(source, encoding)))
+        except ParseError, e:
+            raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
+        self.filters = [self._flatten, self._eval]
+        if loader:
+            self.filters.append(self._include)
+
+    def __repr__(self):
+        return '<%s "%s">' % (self.__class__.__name__, self.filename)
+
+    def _parse(self, source, encoding):
+        """Parse the template.
+        
+        The parsing stage parses the template and constructs a list of
+        directives that will be executed in the render stage. The input is
+        split up into literal output (text that does not depend on the context
+        data) and directives or expressions.
+        
+        :param source: a file-like object containing the XML source of the
+                       template, or an XML event stream
+        :param encoding: the encoding of the `source`
+        """
+        raise NotImplementedError
+
+    def _prepare(self, stream):
+        """Call the `attach` method of every directive found in the template.
+        
+        :param stream: the event stream of the template
+        """
+        for kind, data, pos in stream:
+            if kind is SUB:
+                directives = []
+                substream = data[1]
+                for cls, value, namespaces, pos in data[0]:
+                    directive, substream = cls.attach(self, substream, value,
+                                                      namespaces, pos)
+                    if directive:
+                        directives.append(directive)
+                substream = self._prepare(substream)
+                if directives:
+                    yield kind, (directives, list(substream)), pos
+                else:
+                    for event in substream:
+                        yield event
+            else:
+                if kind is INCLUDE:
+                    data = data[0], list(self._prepare(data[1]))
+                yield kind, data, pos
+
+    def generate(self, *args, **kwargs):
+        """Apply the template to the given context data.
+        
+        Any keyword arguments are made available to the template as context
+        data.
+        
+        Only one positional argument is accepted: if it is provided, it must be
+        an instance of the `Context` class, and keyword arguments are ignored.
+        This calling style is used for internal processing.
+        
+        :return: a markup event stream representing the result of applying
+                 the template to the context data.
+        """
+        if args:
+            assert len(args) == 1
+            ctxt = args[0]
+            if ctxt is None:
+                ctxt = Context(**kwargs)
+            assert isinstance(ctxt, Context)
+        else:
+            ctxt = Context(**kwargs)
+
+        stream = self.stream
+        for filter_ in self.filters:
+            stream = filter_(iter(stream), ctxt)
+        return Stream(stream)
+
+    def _eval(self, stream, ctxt):
+        """Internal stream filter that evaluates any expressions in `START` and
+        `TEXT` events.
+        """
+        filters = (self._flatten, self._eval)
+
+        for kind, data, pos in stream:
+
+            if kind is START and data[1]:
+                # Attributes may still contain expressions in start tags at
+                # this point, so do some evaluation
+                tag, attrs = data
+                new_attrs = []
+                for name, substream in attrs:
+                    if isinstance(substream, basestring):
+                        value = substream
+                    else:
+                        values = []
+                        for subkind, subdata, subpos in self._eval(substream,
+                                                                   ctxt):
+                            if subkind is TEXT:
+                                values.append(subdata)
+                        value = [x for x in values if x is not None]
+                        if not value:
+                            continue
+                    new_attrs.append((name, u''.join(value)))
+                yield kind, (tag, Attrs(new_attrs)), pos
+
+            elif kind is EXPR:
+                result = data.evaluate(ctxt)
+                if result is not None:
+                    # First check for a string, otherwise the iterable test below
+                    # succeeds, and the string will be chopped up into individual
+                    # characters
+                    if isinstance(result, basestring):
+                        yield TEXT, result, pos
+                    elif hasattr(result, '__iter__'):
+                        substream = _ensure(result)
+                        for filter_ in filters:
+                            substream = filter_(substream, ctxt)
+                        for event in substream:
+                            yield event
+                    else:
+                        yield TEXT, unicode(result), pos
+
+            else:
+                yield kind, data, pos
+
+    def _flatten(self, stream, ctxt):
+        """Internal stream filter that expands `SUB` events in the stream."""
+        for event in stream:
+            if event[0] is SUB:
+                # This event is a list of directives and a list of nested
+                # events to which those directives should be applied
+                directives, substream = event[1]
+                substream = _apply_directives(substream, ctxt, directives)
+                for event in self._flatten(substream, ctxt):
+                    yield event
+            else:
+                yield event
+
+    def _include(self, stream, ctxt):
+        """Internal stream filter that performs inclusion of external
+        template files.
+        """
+        from genshi.template.loader import TemplateNotFound
+
+        for event in stream:
+            if event[0] is INCLUDE:
+                href, fallback = event[1]
+                if not isinstance(href, basestring):
+                    parts = []
+                    for subkind, subdata, subpos in self._eval(href, ctxt):
+                        if subkind is TEXT:
+                            parts.append(subdata)
+                    href = u''.join([x for x in parts if x is not None])
+                try:
+                    tmpl = self.loader.load(href, relative_to=event[2][0],
+                                            cls=self.__class__)
+                    for event in tmpl.generate(ctxt):
+                        yield event
+                except TemplateNotFound:
+                    if fallback is None:
+                        raise
+                    for filter_ in self.filters:
+                        fallback = filter_(iter(fallback), ctxt)
+                    for event in fallback:
+                        yield event
+            else:
+                yield event
+
+
+EXPR = Template.EXPR
+INCLUDE = Template.INCLUDE
+SUB = Template.SUB
deleted file mode 100644
--- a/genshi/template/core.py
+++ /dev/null
@@ -1,368 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
-
-try:
-    from collections import deque
-except ImportError:
-    class deque(list):
-        def appendleft(self, x): self.insert(0, x)
-        def popleft(self): return self.pop(0)
-import imp
-import os
-import re
-from StringIO import StringIO
-
-from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
-from genshi.template.eval import Expression
-
-__all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError',
-           'TemplateSyntaxError', 'BadDirectiveError']
-
-
-class TemplateError(Exception):
-    """Base exception class for errors related to template processing."""
-
-
-class TemplateRuntimeError(TemplateError):
-    """Exception raised when an the evualation of a Python expression in a
-    template causes an error."""
-
-    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
-        self.msg = message
-        message = '%s (%s, line %d)' % (self.msg, filename, lineno)
-        TemplateError.__init__(self, message)
-        self.filename = filename
-        self.lineno = lineno
-        self.offset = offset
-
-
-class TemplateSyntaxError(TemplateError):
-    """Exception raised when an expression in a template causes a Python syntax
-    error."""
-
-    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
-        if isinstance(message, SyntaxError) and message.lineno is not None:
-            message = str(message).replace(' (line %d)' % message.lineno, '')
-        self.msg = message
-        message = '%s (%s, line %d)' % (self.msg, filename, lineno)
-        TemplateError.__init__(self, message)
-        self.filename = filename
-        self.lineno = lineno
-        self.offset = offset
-
-
-class BadDirectiveError(TemplateSyntaxError):
-    """Exception raised when an unknown directive is encountered when parsing
-    a template.
-    
-    An unknown directive is any attribute using the namespace for directives,
-    with a local name that doesn't match any registered directive.
-    """
-
-    def __init__(self, name, filename='<string>', lineno=-1):
-        message = 'bad directive "%s"' % name
-        TemplateSyntaxError.__init__(self, message, filename, lineno)
-
-
-class Context(object):
-    """Container for template input data.
-    
-    A context provides a stack of scopes (represented by dictionaries).
-    
-    Template directives such as loops can push a new scope on the stack with
-    data that should only be available inside the loop. When the loop
-    terminates, that scope can get popped off the stack again.
-    
-    >>> ctxt = Context(one='foo', other=1)
-    >>> ctxt.get('one')
-    'foo'
-    >>> ctxt.get('other')
-    1
-    >>> ctxt.push(dict(one='frost'))
-    >>> ctxt.get('one')
-    'frost'
-    >>> ctxt.get('other')
-    1
-    >>> ctxt.pop()
-    {'one': 'frost'}
-    >>> ctxt.get('one')
-    'foo'
-    """
-
-    def __init__(self, **data):
-        self.frames = deque([data])
-        self.pop = self.frames.popleft
-        self.push = self.frames.appendleft
-        self._match_templates = []
-
-    def __repr__(self):
-        return repr(list(self.frames))
-
-    def __setitem__(self, key, value):
-        """Set a variable in the current scope."""
-        self.frames[0][key] = value
-
-    def _find(self, key, default=None):
-        """Retrieve a given variable's value and the frame it was found in.
-
-        Intented for internal use by directives.
-        """
-        for frame in self.frames:
-            if key in frame:
-                return frame[key], frame
-        return default, None
-
-    def get(self, key, default=None):
-        """Get a variable's value, starting at the current scope and going
-        upward.
-        """
-        for frame in self.frames:
-            if key in frame:
-                return frame[key]
-        return default
-    __getitem__ = get
-
-    def push(self, data):
-        """Push a new scope on the stack."""
-
-    def pop(self):
-        """Pop the top-most scope from the stack."""
-
-
-def _apply_directives(stream, ctxt, directives):
-    """Apply the given directives to the stream."""
-    if directives:
-        stream = directives[0](iter(stream), ctxt, directives[1:])
-    return stream
-
-
-class TemplateMeta(type):
-    """Meta class for templates."""
-
-    def __new__(cls, name, bases, d):
-        if 'directives' in d:
-            d['_dir_by_name'] = dict(d['directives'])
-            d['_dir_order'] = [directive[1] for directive in d['directives']]
-
-        return type.__new__(cls, name, bases, d)
-
-
-class Template(object):
-    """Abstract template base class.
-    
-    This class implements most of the template processing model, but does not
-    specify the syntax of templates.
-    """
-    __metaclass__ = TemplateMeta
-
-    EXPR = StreamEventKind('EXPR') # an expression
-    SUB = StreamEventKind('SUB') # a "subprogram"
-
-    def __init__(self, source, basedir=None, filename=None, loader=None,
-                 encoding=None):
-        """Initialize a template from either a string or a file-like object."""
-        self.basedir = basedir
-        self.filename = filename
-        if basedir and filename:
-            self.filepath = os.path.join(basedir, filename)
-        else:
-            self.filepath = filename
-        self.loader = loader
-
-        if isinstance(source, basestring):
-            source = StringIO(source)
-        else:
-            source = source
-        self.stream = list(self._prepare(self._parse(source, encoding)))
-        self.filters = [self._flatten, self._eval]
-
-    def __repr__(self):
-        return '<%s "%s">' % (self.__class__.__name__, self.filename)
-
-    def _parse(self, source, encoding):
-        """Parse the template.
-        
-        The parsing stage parses the template and constructs a list of
-        directives that will be executed in the render stage. The input is
-        split up into literal output (text that does not depend on the context
-        data) and directives or expressions.
-        """
-        raise NotImplementedError
-
-    _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}', re.DOTALL)
-    _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z_][a-zA-Z0-9_\.]*)')
-
-    def _interpolate(cls, text, basedir=None, filename=None, lineno=-1,
-                     offset=0):
-        """Parse the given string and extract expressions.
-        
-        This method returns a list containing both literal text and `Expression`
-        objects.
-        
-        @param text: the text to parse
-        @param lineno: the line number at which the text was found (optional)
-        @param offset: the column number at which the text starts in the source
-            (optional)
-        """
-        filepath = filename
-        if filepath and basedir:
-            filepath = os.path.join(basedir, filepath)
-        def _interpolate(text, patterns, lineno=lineno, offset=offset):
-            for idx, grp in enumerate(patterns.pop(0).split(text)):
-                if idx % 2:
-                    try:
-                        yield EXPR, Expression(grp.strip(), filepath, lineno), \
-                              (filename, lineno, offset)
-                    except SyntaxError, err:
-                        raise TemplateSyntaxError(err, filepath, lineno,
-                                                  offset + (err.offset or 0))
-                elif grp:
-                    if patterns:
-                        for result in _interpolate(grp, patterns[:]):
-                            yield result
-                    else:
-                        yield TEXT, grp.replace('$$', '$'), \
-                              (filename, lineno, offset)
-                if '\n' in grp:
-                    lines = grp.splitlines()
-                    lineno += len(lines) - 1
-                    offset += len(lines[-1])
-                else:
-                    offset += len(grp)
-        return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE])
-    _interpolate = classmethod(_interpolate)
-
-    def _prepare(self, stream):
-        """Call the `attach` method of every directive found in the template."""
-        for kind, data, pos in stream:
-            if kind is SUB:
-                directives = []
-                substream = data[1]
-                for cls, value, namespaces, pos in data[0]:
-                    directive, substream = cls.attach(self, substream, value,
-                                                      namespaces, pos)
-                    if directive:
-                        directives.append(directive)
-                substream = self._prepare(substream)
-                if directives:
-                    yield kind, (directives, list(substream)), pos
-                else:
-                    for event in substream:
-                        yield event
-            else:
-                yield kind, data, pos
-
-    def compile(self):
-        """Compile the template to a Python module, and return the module
-        object.
-        """
-        from genshi.template.inline import inline
-
-        name = (self.filename or '_some_ident').replace('.', '_')
-        module = imp.new_module(name)
-        source = u'\n'.join(list(inline(self)))
-        code = compile(source, self.filepath or '<string>', 'exec')
-        exec code in module.__dict__, module.__dict__
-        return module
-
-    def generate(self, *args, **kwargs):
-        """Apply the template to the given context data.
-        
-        Any keyword arguments are made available to the template as context
-        data.
-        
-        Only one positional argument is accepted: if it is provided, it must be
-        an instance of the `Context` class, and keyword arguments are ignored.
-        This calling style is used for internal processing.
-        
-        @return: a markup event stream representing the result of applying
-            the template to the context data.
-        """
-        if args:
-            assert len(args) == 1
-            ctxt = args[0]
-            if ctxt is None:
-                ctxt = Context(**kwargs)
-            assert isinstance(ctxt, Context)
-        else:
-            ctxt = Context(**kwargs)
-
-        stream = self.stream
-        for filter_ in self.filters:
-            stream = filter_(iter(stream), ctxt)
-        return Stream(stream)
-
-    def _eval(self, stream, ctxt):
-        """Internal stream filter that evaluates any expressions in `START` and
-        `TEXT` events.
-        """
-        filters = (self._flatten, self._eval)
-
-        for kind, data, pos in stream:
-
-            if kind is START and data[1]:
-                # Attributes may still contain expressions in start tags at
-                # this point, so do some evaluation
-                tag, attrs = data
-                new_attrs = []
-                for name, substream in attrs:
-                    if isinstance(substream, basestring):
-                        value = substream
-                    else:
-                        values = []
-                        for subkind, subdata, subpos in self._eval(substream,
-                                                                   ctxt):
-                            if subkind is TEXT:
-                                values.append(subdata)
-                        value = [x for x in values if x is not None]
-                        if not value:
-                            continue
-                    new_attrs.append((name, u''.join(value)))
-                yield kind, (tag, Attrs(new_attrs)), pos
-
-            elif kind is EXPR:
-                result = data.evaluate(ctxt)
-                if result is not None:
-                    # First check for a string, otherwise the iterable test below
-                    # succeeds, and the string will be chopped up into individual
-                    # characters
-                    if isinstance(result, basestring):
-                        yield TEXT, result, pos
-                    elif hasattr(result, '__iter__'):
-                        substream = _ensure(result)
-                        for filter_ in filters:
-                            substream = filter_(substream, ctxt)
-                        for event in substream:
-                            yield event
-                    else:
-                        yield TEXT, unicode(result), pos
-
-            else:
-                yield kind, data, pos
-
-    def _flatten(self, stream, ctxt):
-        """Internal stream filter that expands `SUB` events in the stream."""
-        for event in stream:
-            if event[0] is SUB:
-                # This event is a list of directives and a list of nested
-                # events to which those directives should be applied
-                directives, substream = event[1]
-                substream = _apply_directives(substream, ctxt, directives)
-                for event in self._flatten(substream, ctxt):
-                    yield event
-            else:
-                yield event
-
-
-EXPR = Template.EXPR
-SUB = Template.SUB
--- a/genshi/template/directives.py
+++ b/genshi/template/directives.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -15,9 +15,9 @@
 
 import compiler
 
-from genshi.core import Attrs, Stream
+from genshi.core import Attrs, QName, Stream
 from genshi.path import Path
-from genshi.template.core import TemplateRuntimeError, TemplateSyntaxError, \
+from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \
                                  EXPR, _apply_directives
 from genshi.template.eval import Expression, _parse
 
@@ -25,6 +25,7 @@
            'DefDirective', 'ForDirective', 'IfDirective', 'MatchDirective',
            'OtherwiseDirective', 'ReplaceDirective', 'StripDirective',
            'WhenDirective', 'WithDirective']
+__docformat__ = 'restructuredtext en'
 
 
 class DirectiveMeta(type):
@@ -39,8 +40,8 @@
     """Abstract base class for template directives.
     
     A directive is basically a callable that takes three positional arguments:
-    `ctxt` is the template data context, `stream` is an iterable over the
-    events that the directive applies to, and `directives` is is a list of
+    ``ctxt`` is the template data context, ``stream`` is an iterable over the
+    events that the directive applies to, and ``directives`` is is a list of
     other directives on the same stream that need to be applied.
     
     Directives can be "anonymous" or "registered". Registered directives can be
@@ -56,36 +57,36 @@
     __metaclass__ = DirectiveMeta
     __slots__ = ['expr']
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+    def __init__(self, value, template=None, namespaces=None, lineno=-1,
                  offset=-1):
-        self.expr = self._parse_expr(value, filename, lineno, offset)
+        self.expr = self._parse_expr(value, template, lineno, offset)
 
     def attach(cls, template, stream, value, namespaces, pos):
         """Called after the template stream has been completely parsed.
         
-        @param template: the `Template` object
-        @param stream: the event stream associated with the directive
-        @param value: the argument value for the directive
-        @param namespaces: a mapping of namespace URIs to prefixes
-        @param pos: a `(filename, lineno, offset)` tuple describing the location
-            where the directive was found in the source
+        :param template: the `Template` object
+        :param stream: the event stream associated with the directive
+        :param value: the argument value for the directive
+        :param namespaces: a mapping of namespace URIs to prefixes
+        :param pos: a ``(filename, lineno, offset)`` tuple describing the
+                    location where the directive was found in the source
         
-        This class method should return a `(directive, stream)` tuple. If
-        `directive` is not `None`, it should be an instance of the `Directive`
+        This class method should return a ``(directive, stream)`` tuple. If
+        ``directive`` is not ``None``, it should be an instance of the `Directive`
         class, and gets added to the list of directives applied to the substream
         at runtime. `stream` is an event stream that replaces the original
         stream associated with the directive.
         """
-        return cls(value, namespaces, template.filename, *pos[1:]), stream
+        return cls(value, template, namespaces, *pos[1:]), stream
     attach = classmethod(attach)
 
     def __call__(self, stream, ctxt, directives):
         """Apply the directive to the given stream.
         
-        @param stream: the event stream
-        @param ctxt: the context data
-        @param directives: a list of the remaining directives that should
-            process the stream
+        :param stream: the event stream
+        :param ctxt: the context data
+        :param directives: a list of the remaining directives that should
+                           process the stream
         """
         raise NotImplementedError
 
@@ -95,16 +96,17 @@
             expr = ' "%s"' % self.expr.source
         return '<%s%s>' % (self.__class__.__name__, expr)
 
-    def _parse_expr(cls, expr, filename=None, lineno=-1, offset=-1):
+    def _parse_expr(cls, expr, template, lineno=-1, offset=-1):
         """Parses the given expression, raising a useful error message when a
         syntax error is encountered.
         """
         try:
-            return expr and Expression(expr, filename, lineno) or None
+            return expr and Expression(expr, template.filepath, lineno,
+                                       lookup=template.lookup) or None
         except SyntaxError, err:
             err.msg += ' in expression "%s" of "%s" directive' % (expr,
                                                                   cls.tagname)
-            raise TemplateSyntaxError(err, filename, lineno,
+            raise TemplateSyntaxError(err, template.filepath, lineno,
                                       offset + (err.offset or 0))
     _parse_expr = classmethod(_parse_expr)
 
@@ -128,10 +130,10 @@
 
 
 class AttrsDirective(Directive):
-    """Implementation of the `py:attrs` template directive.
+    """Implementation of the ``py:attrs`` template directive.
     
-    The value of the `py:attrs` attribute should be a dictionary or a sequence
-    of `(name, value)` tuples. The items in that dictionary or sequence are
+    The value of the ``py:attrs`` attribute should be a dictionary or a sequence
+    of ``(name, value)`` tuples. The items in that dictionary or sequence are
     added as attributes to the element:
     
     >>> from genshi.template import MarkupTemplate
@@ -147,7 +149,7 @@
       <li class="collapse">Bar</li>
     </ul>
     
-    If the value evaluates to `None` (or any other non-truth value), no
+    If the value evaluates to ``None`` (or any other non-truth value), no
     attributes are added:
     
     >>> print tmpl.generate(foo=None)
@@ -170,8 +172,8 @@
                 elif not isinstance(attrs, list): # assume it's a dict
                     attrs = attrs.items()
                 attrib -= [name for name, val in attrs if val is None]
-                attrib |= [(name, unicode(val).strip()) for name, val in attrs
-                           if val is not None]
+                attrib |= [(QName(name), unicode(val).strip()) for name, val
+                           in attrs if val is not None]
             yield kind, (tag, attrib), pos
             for event in stream:
                 yield event
@@ -180,10 +182,10 @@
 
 
 class ContentDirective(Directive):
-    """Implementation of the `py:content` template directive.
+    """Implementation of the ``py:content`` template directive.
     
     This directive replaces the content of the element with the result of
-    evaluating the value of the `py:content` attribute:
+    evaluating the value of the ``py:content`` attribute:
     
     >>> from genshi.template import MarkupTemplate
     >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
@@ -197,13 +199,13 @@
     __slots__ = []
 
     def attach(cls, template, stream, value, namespaces, pos):
-        expr = cls._parse_expr(value, template.filename, *pos[1:])
+        expr = cls._parse_expr(value, template, *pos[1:])
         return None, [stream[0], (EXPR, expr, pos),  stream[-1]]
     attach = classmethod(attach)
 
 
 class DefDirective(Directive):
-    """Implementation of the `py:def` template directive.
+    """Implementation of the ``py:def`` template directive.
     
     This directive can be used to create "Named Template Functions", which
     are template snippets that are not actually output during normal
@@ -243,26 +245,34 @@
       </p>
     </div>
     """
-    __slots__ = ['name', 'args', 'defaults', 'signature']
+    __slots__ = ['name', 'args', 'star_args', 'dstar_args', 'defaults',
+                 'signature']
 
     ATTRIBUTE = 'function'
 
-    def __init__(self, args, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
+    def __init__(self, args, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
         self.signature = args.strip()
         ast = _parse(args).node
         self.args = []
+        self.star_args = None
+        self.dstar_args = None
         self.defaults = {}
         if isinstance(ast, compiler.ast.CallFunc):
             self.name = ast.node.name
             for arg in ast.args:
                 if isinstance(arg, compiler.ast.Keyword):
                     self.args.append(arg.name)
-                    self.defaults[arg.name] = Expression(arg.expr, filename,
-                                                         lineno)
+                    self.defaults[arg.name] = Expression(arg.expr,
+                                                         template.filepath,
+                                                         lineno,
+                                                         lookup=template.lookup)
                 else:
                     self.args.append(arg.name)
+            if ast.star_args:
+                self.star_args = ast.star_args.name
+            if ast.dstar_args:
+                self.dstar_args = ast.dstar_args.name
         else:
             self.name = ast.name
 
@@ -281,6 +291,10 @@
                     else:
                         val = self.defaults.get(name).evaluate(ctxt)
                     scope[name] = val
+            if not self.star_args is None:
+                scope[self.star_args] = args
+            if not self.dstar_args is None:
+                scope[self.dstar_args] = kwargs
             ctxt.push(scope)
             for event in _apply_directives(stream, ctxt, directives):
                 yield event
@@ -303,7 +317,7 @@
 
 
 class ForDirective(Directive):
-    """Implementation of the `py:for` template directive for repeating an
+    """Implementation of the ``py:for`` template directive for repeating an
     element based on an iterable in the context data.
     
     >>> from genshi.template import MarkupTemplate
@@ -319,16 +333,15 @@
 
     ATTRIBUTE = 'each'
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
         if ' in ' not in value:
             raise TemplateSyntaxError('"in" keyword missing in "for" directive',
-                                      filename, lineno, offset)
+                                      template.filepath, lineno, offset)
         assign, value = value.split(' in ', 1)
         self.target = _parse(assign, 'exec').node.nodes[0].expr
         self.assign = _assignment(self.target)
-        self.filename = filename
-        Directive.__init__(self, value.strip(), namespaces, filename, lineno,
+        self.filename = template.filepath
+        Directive.__init__(self, value.strip(), template, namespaces, lineno,
                            offset)
 
     def __call__(self, stream, ctxt, directives):
@@ -355,7 +368,7 @@
 
 
 class IfDirective(Directive):
-    """Implementation of the `py:if` template directive for conditionally
+    """Implementation of the ``py:if`` template directive for conditionally
     excluding elements from being output.
     
     >>> from genshi.template import MarkupTemplate
@@ -378,7 +391,7 @@
 
 
 class MatchDirective(Directive):
-    """Implementation of the `py:match` template directive.
+    """Implementation of the ``py:match`` template directive.
 
     >>> from genshi.template import MarkupTemplate
     >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
@@ -398,10 +411,9 @@
 
     ATTRIBUTE = 'path'
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
-        self.path = Path(value, filename, lineno)
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
+        self.path = Path(value, template.filepath, lineno)
         self.namespaces = namespaces or {}
 
     def __call__(self, stream, ctxt, directives):
@@ -415,10 +427,10 @@
 
 
 class ReplaceDirective(Directive):
-    """Implementation of the `py:replace` template directive.
+    """Implementation of the ``py:replace`` template directive.
     
     This directive replaces the element with the result of evaluating the
-    value of the `py:replace` attribute:
+    value of the ``py:replace`` attribute:
     
     >>> from genshi.template import MarkupTemplate
     >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
@@ -429,7 +441,7 @@
       Bye
     </div>
     
-    This directive is equivalent to `py:content` combined with `py:strip`,
+    This directive is equivalent to ``py:content`` combined with ``py:strip``,
     providing a less verbose way to achieve the same effect:
     
     >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
@@ -443,16 +455,19 @@
     __slots__ = []
 
     def attach(cls, template, stream, value, namespaces, pos):
-        expr = cls._parse_expr(value, template.filename, *pos[1:])
+        if not value:
+            raise TemplateSyntaxError('missing value for "replace" directive',
+                                      template.filepath, *pos[1:])
+        expr = cls._parse_expr(value, template, *pos[1:])
         return None, [(EXPR, expr, pos)]
     attach = classmethod(attach)
 
 
 class StripDirective(Directive):
-    """Implementation of the `py:strip` template directive.
+    """Implementation of the ``py:strip`` template directive.
     
-    When the value of the `py:strip` attribute evaluates to `True`, the element
-    is stripped from the output
+    When the value of the ``py:strip`` attribute evaluates to ``True``, the
+    element is stripped from the output
     
     >>> from genshi.template import MarkupTemplate
     >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
@@ -503,13 +518,13 @@
 
 
 class ChooseDirective(Directive):
-    """Implementation of the `py:choose` directive for conditionally selecting
+    """Implementation of the ``py:choose`` directive for conditionally selecting
     one of several body elements to display.
     
-    If the `py:choose` expression is empty the expressions of nested `py:when`
-    directives are tested for truth.  The first true `py:when` body is output.
-    If no `py:when` directive is matched then the fallback directive
-    `py:otherwise` will be used.
+    If the ``py:choose`` expression is empty the expressions of nested
+    ``py:when`` directives are tested for truth.  The first true ``py:when``
+    body is output. If no ``py:when`` directive is matched then the fallback
+    directive ``py:otherwise`` will be used.
     
     >>> from genshi.template import MarkupTemplate
     >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
@@ -523,8 +538,9 @@
       <span>1</span>
     </div>
     
-    If the `py:choose` directive contains an expression, the nested `py:when`
-    directives are tested for equality to the `py:choose` expression:
+    If the ``py:choose`` directive contains an expression, the nested
+    ``py:when`` directives are tested for equality to the ``py:choose``
+    expression:
     
     >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
     ...   py:choose="2">
@@ -536,9 +552,9 @@
       <span>2</span>
     </div>
     
-    Behavior is undefined if a `py:choose` block contains content outside a
-    `py:when` or `py:otherwise` block.  Behavior is also undefined if a
-    `py:otherwise` occurs before `py:when` blocks.
+    Behavior is undefined if a ``py:choose`` block contains content outside a
+    ``py:when`` or ``py:otherwise`` block.  Behavior is also undefined if a
+    ``py:otherwise`` occurs before ``py:when`` blocks.
     """
     __slots__ = ['matched', 'value']
 
@@ -555,19 +571,18 @@
 
 
 class WhenDirective(Directive):
-    """Implementation of the `py:when` directive for nesting in a parent with
-    the `py:choose` directive.
+    """Implementation of the ``py:when`` directive for nesting in a parent with
+    the ``py:choose`` directive.
     
-    See the documentation of `py:choose` for usage.
+    See the documentation of the `ChooseDirective` for usage.
     """
     __slots__ = ['filename']
 
     ATTRIBUTE = 'test'
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, value, namespaces, filename, lineno, offset)
-        self.filename = filename
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, value, template, namespaces, lineno, offset)
+        self.filename = template.filepath
 
     def __call__(self, stream, ctxt, directives):
         matched, frame = ctxt._find('_choose.matched')
@@ -597,17 +612,16 @@
 
 
 class OtherwiseDirective(Directive):
-    """Implementation of the `py:otherwise` directive for nesting in a parent
-    with the `py:choose` directive.
+    """Implementation of the ``py:otherwise`` directive for nesting in a parent
+    with the ``py:choose`` directive.
     
-    See the documentation of `py:choose` for usage.
+    See the documentation of `ChooseDirective` for usage.
     """
     __slots__ = ['filename']
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
-        self.filename = filename
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
+        self.filename = template.filepath
 
     def __call__(self, stream, ctxt, directives):
         matched, frame = ctxt._find('_choose.matched')
@@ -623,7 +637,7 @@
 
 
 class WithDirective(Directive):
-    """Implementation of the `py:with` template directive, which allows
+    """Implementation of the ``py:with`` template directive, which allows
     shorthand access to variables and expressions.
     
     >>> from genshi.template import MarkupTemplate
@@ -639,9 +653,8 @@
 
     ATTRIBUTE = 'vars'
 
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
+    def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
         self.vars = []
         value = value.strip()
         try:
@@ -652,13 +665,14 @@
                 elif not isinstance(node, compiler.ast.Assign):
                     raise TemplateSyntaxError('only assignment allowed in '
                                               'value of the "with" directive',
-                                              filename, lineno, offset)
+                                              template.filepath, lineno, offset)
                 self.vars.append(([(n, _assignment(n)) for n in node.nodes],
-                                  Expression(node.expr, filename, lineno)))
+                                  Expression(node.expr, template.filepath,
+                                             lineno, lookup=template.lookup)))
         except SyntaxError, err:
             err.msg += ' in expression "%s" of "%s" directive' % (value,
                                                                   self.tagname)
-            raise TemplateSyntaxError(err, filename, lineno,
+            raise TemplateSyntaxError(err, template.filepath, lineno,
                                       offset + (err.offset or 0))
 
     def __call__(self, stream, ctxt, directives):
--- a/genshi/template/eval.py
+++ b/genshi/template/eval.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -15,19 +15,72 @@
 
 import __builtin__
 from compiler import ast, parse
-from compiler.pycodegen import ExpressionCodeGenerator
+from compiler.pycodegen import ExpressionCodeGenerator, ModuleCodeGenerator
 import new
 try:
     set
 except NameError:
     from sets import Set as set
+import sys
 
+from genshi.core import Markup
+from genshi.template.base import TemplateRuntimeError
 from genshi.util import flatten
 
-__all__ = ['Expression', 'Undefined']
+__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
+           'Undefined', 'UndefinedError']
+__docformat__ = 'restructuredtext en'
 
 
-class Expression(object):
+class Code(object):
+    """Abstract base class for the `Expression` and `Suite` classes."""
+    __slots__ = ['source', 'code', '_globals']
+
+    def __init__(self, source, filename=None, lineno=-1, lookup='lenient'):
+        """Create the code object, either from a string, or from an AST node.
+        
+        :param source: either a string containing the source code, or an AST
+                       node
+        :param filename: the (preferably absolute) name of the file containing
+                         the code
+        :param lineno: the number of the line on which the code was found
+        :param lookup: the lookup class that defines how variables are looked
+                       up in the context. Can be either `LenientLookup` (the
+                       default), `StrictLookup`, or a custom lookup class
+        """
+        if isinstance(source, basestring):
+            self.source = source
+            node = _parse(source, mode=self.mode)
+        else:
+            assert isinstance(source, ast.Node)
+            self.source = '?'
+            if self.mode == 'eval':
+                node = ast.Expression(source)
+            else:
+                node = ast.Module(None, source)
+
+        self.code = _compile(node, self.source, mode=self.mode,
+                             filename=filename, lineno=lineno)
+        if lookup is None:
+            lookup = LenientLookup
+        elif isinstance(lookup, basestring):
+            lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup]
+        self._globals = lookup.globals()
+
+    def __eq__(self, other):
+        return (type(other) == type(self)) and (self.code == other.code)
+
+    def __hash__(self):
+        return hash(self.code)
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __repr__(self):
+        return '%s(%r)' % (self.__class__.__name__, self.source)
+
+
+class Expression(Code):
     """Evaluates Python expressions used in templates.
 
     >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
@@ -48,7 +101,7 @@
     'thing'
     
     This also works the other way around: item access can be used to access
-    any object attribute (meaning there's no use for `getattr()` in templates):
+    any object attribute:
     
     >>> class MyClass(object):
     ...     myattr = 'Bar'
@@ -61,57 +114,65 @@
     'Bar'
     
     All of the standard Python operators are available to template expressions.
-    Built-in functions such as `len()` are also available in template
+    Built-in functions such as ``len()`` are also available in template
     expressions:
     
     >>> data = dict(items=[1, 2, 3])
     >>> Expression('len(items)').evaluate(data)
     3
     """
-    __slots__ = ['source', 'code']
-
-    def __init__(self, source, filename=None, lineno=-1):
-        """Create the expression, either from a string, or from an AST node.
-        
-        @param source: either a string containing the source code of the
-            expression, or an AST node
-        @param filename: the (preferably absolute) name of the file containing
-            the expression
-        @param lineno: the number of the line on which the expression was found
-        """
-        if isinstance(source, basestring):
-            self.source = source
-            self.code = _compile(_parse(source), self.source, filename=filename,
-                                 lineno=lineno)
-        else:
-            assert isinstance(source, ast.Node)
-            self.source = '?'
-            self.code = _compile(ast.Expression(source), filename=filename,
-                                 lineno=lineno)
-
-    def __eq__(self, other):
-        return (type(other) == Expression) and (self.code == other.code)
-
-    def __hash__(self):
-        return hash(self.code)
-
-    def __ne__(self, other):
-        return not self == other
-
-    def __repr__(self):
-        return 'Expression(%r)' % self.source
+    __slots__ = []
+    mode = 'eval'
 
     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
+        :param data: a mapping containing the data to evaluate against
+        :return: the result of the evaluation
         """
-        return eval(self.code, {'data': data,
-                                '_lookup_name': _lookup_name,
-                                '_lookup_attr': _lookup_attr,
-                                '_lookup_item': _lookup_item},
-                               {'data': data})
+        __traceback_hide__ = 'before_and_this'
+        _globals = self._globals
+        _globals['data'] = data
+        return eval(self.code, _globals, {'data': data})
+
+
+class Suite(Code):
+    """Executes Python statements used in templates.
+
+    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
+    >>> Suite("foo = dict['some']").execute(data)
+    >>> data['foo']
+    'thing'
+    """
+    __slots__ = []
+    mode = 'exec'
+
+    def execute(self, data):
+        """Execute the suite in the given data dictionary.
+        
+        :param data: a mapping containing the data to execute in
+        """
+        __traceback_hide__ = 'before_and_this'
+        _globals = self._globals
+        _globals['data'] = data
+        exec self.code in _globals, data
+
+
+UNDEFINED = object()
+
+
+class UndefinedError(TemplateRuntimeError):
+    """Exception thrown when a template expression attempts to access a variable
+    not defined in the context.
+    
+    :see: `LenientLookup`, `StrictLookup`
+    """
+    def __init__(self, name, owner=UNDEFINED):
+        if owner is not UNDEFINED:
+            message = '%s has no member named "%s"' % (repr(owner), name)
+        else:
+            message = '"%s" not defined' % name
+        TemplateRuntimeError.__init__(self, message)
 
 
 class Undefined(object):
@@ -119,8 +180,8 @@
     
     Unlike the Python runtime, template expressions can refer to an undefined
     variable without causing a `NameError` to be raised. The result will be an
-    instance of the `UndefinedĀ“ class, which is treated the same as `False` in
-    conditions, and acts as an empty collection in iterations:
+    instance of the `Undefined` class, which is treated the same as ``False`` in
+    conditions, but raise an exception on any other operation:
     
     >>> foo = Undefined('foo')
     >>> bool(foo)
@@ -137,25 +198,25 @@
     >>> foo('bar')
     Traceback (most recent call last):
         ...
-    NameError: Variable "foo" is not defined
+    UndefinedError: "foo" not defined
 
     >>> foo.bar
     Traceback (most recent call last):
         ...
-    NameError: Variable "foo" is not defined
+    UndefinedError: "foo" not defined
+    
+    :see: `LenientLookup`
     """
-    __slots__ = ['_name']
-
-    def __init__(self, name):
-        self._name = name
+    __slots__ = ['_name', '_owner']
 
-    def __call__(self, *args, **kwargs):
-        __traceback_hide__ = True
-        self.throw()
-
-    def __getattr__(self, name):
-        __traceback_hide__ = True
-        self.throw()
+    def __init__(self, name, owner=UNDEFINED):
+        """Initialize the object.
+        
+        :param name: the name of the reference
+        :param owner: the owning object, if the variable is accessed as a member
+        """
+        self._name = name
+        self._owner = owner
 
     def __iter__(self):
         return iter([])
@@ -164,11 +225,134 @@
         return False
 
     def __repr__(self):
+        return '<%s %r>' % (self.__class__.__name__, self._name)
+
+    def __str__(self):
         return 'undefined'
 
-    def throw(self):
+    def _die(self, *args, **kwargs):
+        """Raise an `UndefinedError`."""
         __traceback_hide__ = True
-        raise NameError('Variable "%s" is not defined' % self._name)
+        raise UndefinedError(self._name, self._owner)
+    __call__ = __getattr__ = __getitem__ = _die
+
+
+class LookupBase(object):
+    """Abstract base class for variable lookup implementations."""
+
+    def globals(cls):
+        """Construct the globals dictionary to use as the execution context for
+        the expression or suite.
+        """
+        return {
+            '_lookup_name': cls.lookup_name,
+            '_lookup_attr': cls.lookup_attr,
+            '_lookup_item': cls.lookup_item
+        }
+    globals = classmethod(globals)
+
+    def lookup_name(cls, data, name):
+        __traceback_hide__ = True
+        val = data.get(name, UNDEFINED)
+        if val is UNDEFINED:
+            val = BUILTINS.get(name, val)
+            if val is UNDEFINED:
+                return cls.undefined(name)
+        return val
+    lookup_name = classmethod(lookup_name)
+
+    def lookup_attr(cls, data, obj, key):
+        __traceback_hide__ = True
+        if hasattr(obj, key):
+            return getattr(obj, key)
+        try:
+            return obj[key]
+        except (KeyError, TypeError):
+            return cls.undefined(key, owner=obj)
+    lookup_attr = classmethod(lookup_attr)
+
+    def lookup_item(cls, data, obj, key):
+        __traceback_hide__ = True
+        if len(key) == 1:
+            key = key[0]
+        try:
+            return obj[key]
+        except (AttributeError, KeyError, IndexError, TypeError), e:
+            if isinstance(key, basestring):
+                val = getattr(obj, key, UNDEFINED)
+                if val is UNDEFINED:
+                    return cls.undefined(key, owner=obj)
+                return val
+            raise
+    lookup_item = classmethod(lookup_item)
+
+    def undefined(cls, key, owner=UNDEFINED):
+        """Can be overridden by subclasses to specify behavior when undefined
+        variables are accessed.
+        
+        :param key: the name of the variable
+        :param owner: the owning object, if the variable is accessed as a member
+        """
+        raise NotImplementedError
+    undefined = classmethod(undefined)
+
+
+class LenientLookup(LookupBase):
+    """Default variable lookup mechanism for expressions.
+    
+    When an undefined variable is referenced using this lookup style, the
+    reference evaluates to an instance of the `Undefined` class:
+    
+    >>> expr = Expression('nothing', lookup='lenient')
+    >>> undef = expr.evaluate({})
+    >>> undef
+    <Undefined 'nothing'>
+    
+    The same will happen when a non-existing attribute or item is accessed on
+    an existing object:
+    
+    >>> expr = Expression('something.nil', lookup='lenient')
+    >>> expr.evaluate({'something': dict()})
+    <Undefined 'nil'>
+    
+    See the documentation of the `Undefined` class for details on the behavior
+    of such objects.
+    
+    :see: `StrictLookup`
+    """
+    def undefined(cls, key, owner=UNDEFINED):
+        """Return an ``Undefined`` object."""
+        __traceback_hide__ = True
+        return Undefined(key, owner=owner)
+    undefined = classmethod(undefined)
+
+
+class StrictLookup(LookupBase):
+    """Strict variable lookup mechanism for expressions.
+    
+    Referencing an undefined variable using this lookup style will immediately
+    raise an ``UndefinedError``:
+    
+    >>> expr = Expression('nothing', lookup='strict')
+    >>> expr.evaluate({})
+    Traceback (most recent call last):
+        ...
+    UndefinedError: "nothing" not defined
+    
+    The same happens when a non-existing attribute or item is accessed on an
+    existing object:
+    
+    >>> expr = Expression('something.nil', lookup='strict')
+    >>> expr.evaluate({'something': dict()})
+    Traceback (most recent call last):
+        ...
+    UndefinedError: {} has no member named "nil"
+    """
+    def undefined(cls, key, owner=UNDEFINED):
+        """Raise an ``UndefinedError`` immediately."""
+        __traceback_hide__ = True
+        raise UndefinedError(key, owner=owner)
+    undefined = classmethod(undefined)
 
 
 def _parse(source, mode='eval'):
@@ -176,8 +360,9 @@
         source = '\xef\xbb\xbf' + source.encode('utf-8')
     return parse(source, mode)
 
-def _compile(node, source=None, filename=None, lineno=-1):
-    tree = ExpressionASTTransformer().visit(node)
+def _compile(node, source=None, mode='eval', filename=None, lineno=-1):
+    xform = {'eval': ExpressionASTTransformer}.get(mode, TemplateASTTransformer)
+    tree = xform().visit(node)
     if isinstance(filename, unicode):
         # unicode file names not allowed for code objects
         filename = filename.encode('utf-8', 'replace')
@@ -187,7 +372,12 @@
     if lineno <= 0:
         lineno = 1
 
-    gen = ExpressionCodeGenerator(tree)
+    if mode == 'eval':
+        gen = ExpressionCodeGenerator(tree)
+        name = '<Expression %s>' % (repr(source or '?'))
+    else:
+        gen = ModuleCodeGenerator(tree)
+        name = '<Suite>'
     gen.optimized = True
     code = gen.getCode()
 
@@ -195,49 +385,11 @@
     # clone the code object while adjusting the line number
     return new.code(0, code.co_nlocals, code.co_stacksize,
                     code.co_flags | 0x0040, code.co_code, code.co_consts,
-                    code.co_names, code.co_varnames, filename,
-                    '<Expression %s>' % (repr(source or '?').replace("'", '"')),
-                    lineno, code.co_lnotab, (), ())
+                    code.co_names, code.co_varnames, filename, name, lineno,
+                    code.co_lnotab, (), ())
 
 BUILTINS = __builtin__.__dict__.copy()
-BUILTINS['Undefined'] = Undefined
-_UNDEF = Undefined(None)
-
-def _lookup_name(data, name):
-    __traceback_hide__ = True
-    val = data.get(name, _UNDEF)
-    if val is _UNDEF:
-        val = BUILTINS.get(name, val)
-        if val is _UNDEF:
-            return Undefined(name)
-    return val
-
-def _lookup_attr(data, obj, key):
-    __traceback_hide__ = True
-    if type(obj) is Undefined:
-        obj.throw()
-    if hasattr(obj, key):
-        return getattr(obj, key)
-    try:
-        return obj[key]
-    except (KeyError, TypeError):
-        return Undefined(key)
-
-def _lookup_item(data, obj, key):
-    __traceback_hide__ = True
-    if type(obj) is Undefined:
-        obj.throw()
-    if len(key) == 1:
-        key = key[0]
-    try:
-        return obj[key]
-    except (KeyError, IndexError, TypeError), e:
-        if isinstance(key, basestring):
-            val = getattr(obj, key, _UNDEF)
-            if val is _UNDEF:
-                val = Undefined(key)
-            return val
-        raise
+BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
 
 
 class ASTTransformer(object):
@@ -246,20 +398,32 @@
     Every visitor method can be overridden to return an AST node that has been
     altered or replaced in some way.
     """
-    _visitors = {}
 
     def visit(self, node):
-        v = self._visitors.get(node.__class__)
-        if not v:
-            v = getattr(self, 'visit%s' % node.__class__.__name__)
-            self._visitors[node.__class__] = v
-        return v(node)
+        if node is None:
+            return None
+        if type(node) is tuple:
+            return tuple([self.visit(n) for n in node])
+        visitor = getattr(self, 'visit%s' % node.__class__.__name__,
+                          self._visitDefault)
+        return visitor(node)
+
+    def _visitDefault(self, node):
+        return node
 
     def visitExpression(self, node):
         node.node = self.visit(node.node)
         return node
 
-    # Functions & Accessors
+    def visitModule(self, node):
+        node.node = self.visit(node.node)
+        return node
+
+    def visitStmt(self, node):
+        node.nodes = [self.visit(x) for x in node.nodes]
+        return node
+
+    # Classes, Functions & Accessors
 
     def visitCallFunc(self, node):
         node.node = self.visit(node.node)
@@ -270,7 +434,16 @@
             node.dstar_args = self.visit(node.dstar_args)
         return node
 
-    def visitLambda(self, node):
+    def visitClass(self, node):
+        node.bases = [self.visit(x) for x in node.bases]
+        node.code = self.visit(node.code)
+        node.filename = '<string>' # workaround for bug in pycodegen
+        return node
+
+    def visitFunction(self, node):
+        if hasattr(node, 'decorators'):
+            node.decorators = self.visit(node.decorators)
+        node.defaults = [self.visit(x) for x in node.defaults]
         node.code = self.visit(node.code)
         node.filename = '<string>' # workaround for bug in pycodegen
         return node
@@ -279,18 +452,109 @@
         node.expr = self.visit(node.expr)
         return node
 
+    def visitLambda(self, node):
+        node.code = self.visit(node.code)
+        node.filename = '<string>' # workaround for bug in pycodegen
+        return node
+
     def visitSubscript(self, node):
         node.expr = self.visit(node.expr)
         node.subs = [self.visit(x) for x in node.subs]
         return node
 
+    # Statements
+
+    def visitAssert(self, node):
+        node.test = self.visit(node.test)
+        node.fail = self.visit(node.fail)
+        return node
+
+    def visitAssign(self, node):
+        node.nodes = [self.visit(x) for x in node.nodes]
+        node.expr = self.visit(node.expr)
+        return node
+
+    def visitAssAttr(self, node):
+        node.expr = self.visit(node.expr)
+        return node
+
+    def visitAugAssign(self, node):
+        node.node = self.visit(node.node)
+        node.expr = self.visit(node.expr)
+        return node
+
+    def visitDecorators(self, node):
+        node.nodes = [self.visit(x) for x in node.nodes]
+        return node
+
+    def visitExec(self, node):
+        node.expr = self.visit(node.expr)
+        node.locals = self.visit(node.locals)
+        node.globals = self.visit(node.globals)
+        return node
+
+    def visitFor(self, node):
+        node.assign = self.visit(node.assign)
+        node.list = self.visit(node.list)
+        node.body = self.visit(node.body)
+        node.else_ = self.visit(node.else_)
+        return node
+
+    def visitIf(self, node):
+        node.tests = [self.visit(x) for x in node.tests]
+        node.else_ = self.visit(node.else_)
+        return node
+
+    def _visitPrint(self, node):
+        node.nodes = [self.visit(x) for x in node.nodes]
+        node.dest = self.visit(node.dest)
+        return node
+    visitPrint = visitPrintnl = _visitPrint
+
+    def visitRaise(self, node):
+        node.expr1 = self.visit(node.expr1)
+        node.expr2 = self.visit(node.expr2)
+        node.expr3 = self.visit(node.expr3)
+        return node
+
+    def visitReturn(self, node):
+        node.value = self.visit(node.value)
+        return node
+
+    def visitTryExcept(self, node):
+        node.body = self.visit(node.body)
+        node.handlers = self.visit(node.handlers)
+        node.else_ = self.visit(node.else_)
+        return node
+
+    def visitTryFinally(self, node):
+        node.body = self.visit(node.body)
+        node.final = self.visit(node.final)
+        return node
+
+    def visitWhile(self, node):
+        node.test = self.visit(node.test)
+        node.body = self.visit(node.body)
+        node.else_ = self.visit(node.else_)
+        return node
+
+    def visitWith(self, node):
+        node.expr = self.visit(node.expr)
+        node.vars = [self.visit(x) for x in node.vars]
+        node.body = self.visit(node.body)
+        return node
+
+    def visitYield(self, node):
+        node.value = self.visit(node.value)
+        return node
+
     # Operators
 
     def _visitBoolOp(self, node):
         node.nodes = [self.visit(x) for x in node.nodes]
         return node
     visitAnd = visitOr = visitBitand = visitBitor = visitBitxor = _visitBoolOp
-    visitAssTuple = _visitBoolOp
+    visitAssTuple = visitAssList = _visitBoolOp
 
     def _visitBinOp(self, node):
         node.left = self.visit(node.left)
@@ -309,7 +573,7 @@
         node.expr = self.visit(node.expr)
         return node
     visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp
-    visitBackquote = _visitUnaryOp
+    visitBackquote = visitDiscard = _visitUnaryOp
 
     def visitIfExp(self, node):
         node.test = self.visit(node.test)
@@ -319,10 +583,6 @@
 
     # Identifiers, Literals and Comprehensions
 
-    def _visitDefault(self, node):
-        return node
-    visitAssName = visitConst = visitName = _visitDefault
-
     def visitDict(self, node):
         node.items = [(self.visit(k),
                        self.visit(v)) for k, v in node.items]
@@ -388,9 +648,9 @@
         return node
 
 
-class ExpressionASTTransformer(ASTTransformer):
+class TemplateASTTransformer(ASTTransformer):
     """Concrete AST transformer that implements the AST transformations needed
-    for template expressions.
+    for code embedded in templates.
     """
 
     def __init__(self):
@@ -405,7 +665,41 @@
         return node
 
     def visitAssName(self, node):
-        self.locals[-1].add(node.name)
+        if self.locals:
+            self.locals[-1].add(node.name)
+        return node
+
+    def visitAugAssign(self, node):
+        if isinstance(node.node, ast.Name):
+            name = node.node.name
+            node.node = ast.Subscript(ast.Name('data'), 'OP_APPLY',
+                                      [ast.Const(name)])
+            node.expr = self.visit(node.expr)
+            return ast.If([
+                (ast.Compare(ast.Const(name), [('in', ast.Name('data'))]),
+                 ast.Stmt([node]))],
+                ast.Stmt([ast.Raise(ast.CallFunc(ast.Name('UndefinedError'),
+                                                 [ast.Const(name)]),
+                                    None, None)]))
+        else:
+            return ASTTransformer.visitAugAssign(self, node)
+
+    def visitClass(self, node):
+        self.locals.append(set())
+        node = ASTTransformer.visitClass(self, node)
+        self.locals.pop()
+        return node
+
+    def visitFor(self, node):
+        self.locals.append(set())
+        node = ASTTransformer.visitFor(self, node)
+        self.locals.pop()
+        return node
+
+    def visitFunction(self, node):
+        self.locals.append(set(node.argnames))
+        node = ASTTransformer.visitFunction(self, node)
+        self.locals.pop()
         return node
 
     def visitGenExpr(self, node):
@@ -414,12 +708,6 @@
         self.locals.pop()
         return node
 
-    def visitGetattr(self, node):
-        return ast.CallFunc(ast.Name('_lookup_attr'), [
-            ast.Name('data'), self.visit(node.expr),
-            ast.Const(node.attrname)
-        ])
-
     def visitLambda(self, node):
         self.locals.append(set(flatten(node.argnames)))
         node = ASTTransformer.visitLambda(self, node)
@@ -442,6 +730,18 @@
         func_args = [ast.Name('data'), ast.Const(node.name)]
         return ast.CallFunc(ast.Name('_lookup_name'), func_args)
 
+
+class ExpressionASTTransformer(TemplateASTTransformer):
+    """Concrete AST transformer that implements the AST transformations needed
+    for code embedded in templates.
+    """
+
+    def visitGetattr(self, node):
+        return ast.CallFunc(ast.Name('_lookup_attr'), [
+            ast.Name('data'), self.visit(node.expr),
+            ast.Const(node.attrname)
+        ])
+
     def visitSubscript(self, node):
         return ast.CallFunc(ast.Name('_lookup_item'), [
             ast.Name('data'), self.visit(node.expr),
new file mode 100644
--- /dev/null
+++ b/genshi/template/interpolation.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+"""String interpolation routines, i.e. the splitting up a given text into some
+parts that are literal strings, and others that are Python expressions.
+"""
+
+from itertools import chain
+import os
+from tokenize import tokenprog
+
+from genshi.core import TEXT
+from genshi.template.base import TemplateSyntaxError, EXPR
+from genshi.template.eval import Expression
+
+__all__ = ['interpolate']
+__docformat__ = 'restructuredtext en'
+
+NAMESTART = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
+NAMECHARS = NAMESTART + '.0123456789'
+PREFIX = '$'
+
+def interpolate(text, basedir=None, filename=None, lineno=-1, offset=0,
+                lookup='lenient'):
+    """Parse the given string and extract expressions.
+    
+    This function is a generator that yields `TEXT` events for literal strings,
+    and `EXPR` events for expressions, depending on the results of parsing the
+    string.
+    
+    >>> for kind, data, pos in interpolate("hey ${foo}bar"):
+    ...     print kind, `data`
+    TEXT u'hey '
+    EXPR Expression('foo')
+    TEXT u'bar'
+    
+    :param text: the text to parse
+    :param basedir: base directory of the file in which the text was found
+                    (optional)
+    :param filename: basename of the file in which the text was found (optional)
+    :param lineno: the line number at which the text was found (optional)
+    :param offset: the column number at which the text starts in the source
+                   (optional)
+    :param lookup: the variable lookup mechanism; either "lenient" (the
+                   default), "strict", or a custom lookup class
+    :return: a list of `TEXT` and `EXPR` events
+    :raise TemplateSyntaxError: when a syntax error in an expression is
+                                encountered
+    """
+    filepath = filename
+    if filepath and basedir:
+        filepath = os.path.join(basedir, filepath)
+    pos = [filepath, lineno, offset]
+
+    textbuf = []
+    textpos = None
+    for is_expr, chunk in chain(lex(text, pos, filepath), [(True, '')]):
+        if is_expr:
+            if textbuf:
+                yield TEXT, u''.join(textbuf), textpos
+                del textbuf[:]
+                textpos = None
+            if chunk:
+                try:
+                    expr = Expression(chunk.strip(), pos[0], pos[1],
+                                     lookup=lookup)
+                    yield EXPR, expr, tuple(pos)
+                except SyntaxError, err:
+                    raise TemplateSyntaxError(err, filepath, pos[1],
+                                              pos[2] + (err.offset or 0))
+        else:
+            textbuf.append(chunk)
+            if textpos is None:
+                textpos = tuple(pos)
+
+        if '\n' in chunk:
+            lines = chunk.splitlines()
+            pos[1] += len(lines) - 1
+            pos[2] += len(lines[-1])
+        else:
+            pos[2] += len(chunk)
+
+def lex(text, textpos, filepath):
+    offset = pos = 0
+    end = len(text)
+    escaped = False
+
+    while 1:
+        if escaped:
+            offset = text.find(PREFIX, offset + 2)
+            escaped = False
+        else:
+            offset = text.find(PREFIX, pos)
+        if offset < 0 or offset == end - 1:
+            break
+        next = text[offset + 1]
+
+        if next == '{':
+            if offset > pos:
+                yield False, text[pos:offset]
+            pos = offset + 2
+            level = 1
+            while level:
+                match = tokenprog.match(text, pos)
+                if match is None:
+                    raise TemplateSyntaxError('invalid syntax',  filepath,
+                                              *textpos[1:])
+                pos = match.end()
+                tstart, tend = match.regs[3]
+                token = text[tstart:tend]
+                if token == '{':
+                    level += 1
+                elif token == '}':
+                    level -= 1
+            yield True, text[offset + 2:pos - 1]
+
+        elif next in NAMESTART:
+            if offset > pos:
+                yield False, text[pos:offset]
+                pos = offset
+            pos += 1
+            while pos < end:
+                char = text[pos]
+                if char not in NAMECHARS:
+                    break
+                pos += 1
+            yield True, text[offset + 1:pos].strip()
+
+        elif not escaped and next == PREFIX:
+            escaped = True
+            pos = offset + 1
+
+        else:
+            yield False, text[pos:offset + 1]
+            pos = offset + 1
+
+    if pos < end:
+        yield False, text[pos:]
--- a/genshi/template/loader.py
+++ b/genshi/template/loader.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -19,16 +19,22 @@
 except ImportError:
     import dummy_threading as threading
 
-from genshi.template.core import TemplateError
+from genshi.template.base import TemplateError
 from genshi.util import LRUCache
 
 __all__ = ['TemplateLoader', 'TemplateNotFound']
+__docformat__ = 'restructuredtext en'
 
 
 class TemplateNotFound(TemplateError):
     """Exception raised when a specific template file could not be found."""
 
     def __init__(self, name, search_path):
+        """Create the exception.
+        
+        :param name: the filename of the template
+        :param search_path: the search path used to lookup the template
+        """
         TemplateError.__init__(self, 'Template "%s" not found' % name)
         self.search_path = search_path
 
@@ -66,20 +72,30 @@
     >>> os.remove(path)
     """
     def __init__(self, search_path=None, auto_reload=False,
-                 default_encoding=None, max_cache_size=25, default_class=None):
+                 default_encoding=None, max_cache_size=25, default_class=None,
+                 variable_lookup='lenient', callback=None):
         """Create the template laoder.
         
-        @param search_path: a list of absolute path names that should be
-            searched for template files, or a string containing a single
-            absolute path
-        @param auto_reload: whether to check the last modification time of
-            template files, and reload them if they have changed
-        @param default_encoding: the default encoding to assume when loading
-            templates; defaults to UTF-8
-        @param max_cache_size: the maximum number of templates to keep in the
-            cache
-        @param default_class: the default `Template` subclass to use when
-            instantiating templates
+        :param search_path: a list of absolute path names that should be
+                            searched for template files, or a string containing
+                            a single absolute path
+        :param auto_reload: whether to check the last modification time of
+                            template files, and reload them if they have changed
+        :param default_encoding: the default encoding to assume when loading
+                                 templates; defaults to UTF-8
+        :param max_cache_size: the maximum number of templates to keep in the
+                               cache
+        :param default_class: the default `Template` subclass to use when
+                              instantiating templates
+        :param variable_lookup: the variable lookup mechanism; either "lenient"
+                                (the default), "strict", or a custom lookup
+                                class
+        :param callback: (optional) a callback function that is invoked after a
+                         template was initialized by this loader; the function
+                         is passed the template object as only argument. This
+                         callback can be used for example to add any desired
+                         filters to the template
+        :see: `LenientLookup`, `StrictLookup`
         """
         from genshi.template.markup import MarkupTemplate
 
@@ -91,6 +107,10 @@
         self.auto_reload = auto_reload
         self.default_encoding = default_encoding
         self.default_class = default_class or MarkupTemplate
+        self.variable_lookup = variable_lookup
+        if callback is not None and not callable(callback):
+            raise TypeError('The "callback" parameter needs to be callable')
+        self.callback = callback
         self._cache = LRUCache(max_cache_size)
         self._mtime = {}
         self._lock = threading.Lock()
@@ -100,28 +120,31 @@
         
         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.
+        name is an absolute path, the search path is ignored.
         
-        If requested template is not found, a `TemplateNotFound` exception is
-        raised. Otherwise, a `Template` object is returned that represents the
-        parsed template.
+        If the 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
+        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
-        @param cls: the class of the template object to instantiate
-        @param encoding: the encoding of the template to load; defaults to the
-            `default_encoding` of the loader instance
+        :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
+        :param cls: the class of the template object to instantiate
+        :param encoding: the encoding of the template to load; defaults to the
+                         ``default_encoding`` of the loader instance
+        :return: the loaded `Template` instance
+        :raises TemplateNotFound: if a template with the given name could not be
+                                  found
         """
         if cls is None:
             cls = self.default_class
@@ -176,11 +199,14 @@
                             filename = os.path.join(dirname, filename)
                             dirname = ''
                         tmpl = cls(fileobj, basedir=dirname, filename=filename,
-                                   loader=self, encoding=encoding)
+                                   loader=self, lookup=self.variable_lookup,
+                                   encoding=encoding)
+                        if self.callback:
+                            self.callback(tmpl)
+                        self._cache[filename] = tmpl
+                        self._mtime[filename] = os.path.getmtime(filepath)
                     finally:
                         fileobj.close()
-                    self._cache[filename] = tmpl
-                    self._mtime[filename] = os.path.getmtime(filepath)
                     return tmpl
                 except IOError:
                     continue
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -14,15 +14,27 @@
 """Markup templating engine."""
 
 from itertools import chain
+import sys
+from textwrap import dedent
 
 from genshi.core import Attrs, Namespace, Stream, StreamEventKind
-from genshi.core import START, END, START_NS, END_NS, TEXT, COMMENT
+from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT
 from genshi.input import XMLParser
-from genshi.template.core import BadDirectiveError, Template, \
-                                 _apply_directives, SUB
-from genshi.template.loader import TemplateNotFound
+from genshi.template.base import BadDirectiveError, Template, \
+                                 TemplateSyntaxError, _apply_directives, \
+                                 INCLUDE, SUB
+from genshi.template.eval import Suite
+from genshi.template.interpolation import interpolate
 from genshi.template.directives import *
 
+if sys.version_info < (2, 4):
+    _ctxt2dict = lambda ctxt: ctxt.frames[0]
+else:
+    _ctxt2dict = lambda ctxt: ctxt
+
+__all__ = ['MarkupTemplate']
+__docformat__ = 'restructuredtext en'
+
 
 class MarkupTemplate(Template):
     """Implementation of the template language for XML-based templates.
@@ -35,7 +47,8 @@
       <li>1</li><li>2</li><li>3</li>
     </ul>
     """
-    INCLUDE = StreamEventKind('INCLUDE')
+    EXEC = StreamEventKind('EXEC')
+    """Stream event kind representing a Python code suite to execute."""
 
     DIRECTIVE_NAMESPACE = Namespace('http://genshi.edgewall.org/')
     XINCLUDE_NAMESPACE = Namespace('http://www.w3.org/2001/XInclude')
@@ -54,17 +67,17 @@
                   ('strip', StripDirective)]
 
     def __init__(self, source, basedir=None, filename=None, loader=None,
-                 encoding=None):
-        """Initialize a template from either a string or a file-like object."""
+                 encoding=None, lookup='lenient'):
         Template.__init__(self, source, basedir=basedir, filename=filename,
-                          loader=loader, encoding=encoding)
-
-        self.filters.append(self._match)
+                          loader=loader, encoding=encoding, lookup=lookup)
+        # Make sure the include filter comes after the match filter
+        if loader:
+            self.filters.remove(self._include)
+        self.filters += [self._exec, self._match]
         if loader:
             self.filters.append(self._include)
 
     def _parse(self, source, encoding):
-        """Parse the template from an XML document."""
         streams = [[]] # stacked lists of events of the "compiled" template
         dirmap = {} # temporary mapping of directives to elements
         ns_prefix = {}
@@ -118,8 +131,9 @@
                         directives.append((cls, value, ns_prefix.copy(), pos))
                     else:
                         if value:
-                            value = list(self._interpolate(value, self.basedir,
-                                                           *pos))
+                            value = list(interpolate(value, self.basedir,
+                                                     pos[0], pos[1], pos[2],
+                                                     lookup=self.lookup))
                             if len(value) == 1 and value[0][0] is TEXT:
                                 value = value[0][1]
                         else:
@@ -137,7 +151,8 @@
                         include_href = new_attrs.get('href')
                         if not include_href:
                             raise TemplateSyntaxError('Include misses required '
-                                                      'attribute "href"', *pos)
+                                                      'attribute "href"',
+                                                      self.filepath, *pos[1:])
                         streams.append([])
                     elif tag.localname == 'fallback':
                         in_fallback += 1
@@ -169,9 +184,32 @@
                     stream[start_offset:] = [(SUB, (directives, substream),
                                               pos)]
 
+            elif kind is PI and data[0] == 'python':
+                try:
+                    # As Expat doesn't report whitespace between the PI target
+                    # and the data, we have to jump through some hoops here to
+                    # get correctly indented Python code
+                    # Unfortunately, we'll still probably not get the line
+                    # number quite right
+                    lines = [line.expandtabs() for line in data[1].splitlines()]
+                    first = lines[0]
+                    rest = dedent('\n'.join(lines[1:]))
+                    if first.rstrip().endswith(':') and not rest[0].isspace():
+                        rest = '\n'.join(['    ' + line for line
+                                          in rest.splitlines()])
+                    source = '\n'.join([first, rest])
+                    suite = Suite(source, self.filepath, pos[1],
+                                  lookup=self.lookup)
+                except SyntaxError, err:
+                    raise TemplateSyntaxError(err, self.filepath,
+                                              pos[1] + (err.lineno or 1) - 1,
+                                              pos[2] + (err.offset or 0))
+                stream.append((EXEC, suite, pos))
+
             elif kind is TEXT:
-                for kind, data, pos in self._interpolate(data, self.basedir,
-                                                         *pos):
+                for kind, data, pos in interpolate(data, self.basedir, pos[0],
+                                                   pos[1], pos[2],
+                                                   lookup=self.lookup):
                     stream.append((kind, data, pos))
 
             elif kind is COMMENT:
@@ -184,36 +222,13 @@
         assert len(streams) == 1
         return streams[0]
 
-    def _prepare(self, stream):
-        for kind, data, pos in Template._prepare(self, stream):
-            if kind is INCLUDE:
-                data = data[0], list(self._prepare(data[1]))
-            yield kind, data, pos
-
-    def _include(self, stream, ctxt):
-        """Internal stream filter that performs inclusion of external
-        template files.
+    def _exec(self, stream, ctxt):
+        """Internal stream filter that executes code in ``<?python ?>``
+        processing instructions.
         """
         for event in stream:
-            if event[0] is INCLUDE:
-                href, fallback = event[1]
-                if not isinstance(href, basestring):
-                    parts = []
-                    for subkind, subdata, subpos in self._eval(href, ctxt):
-                        if subkind is TEXT:
-                            parts.append(subdata)
-                    href = u''.join([x for x in parts if x is not None])
-                try:
-                    tmpl = self.loader.load(href, relative_to=event[2][0])
-                    for event in tmpl.generate(ctxt):
-                        yield event
-                except TemplateNotFound:
-                    if fallback is None:
-                        raise
-                    for filter_ in self.filters:
-                        fallback = filter_(iter(fallback), ctxt)
-                    for event in fallback:
-                        yield event
+            if event[0] is EXEC:
+                event[1].execute(_ctxt2dict(ctxt))
             else:
                 yield event
 
@@ -291,4 +306,4 @@
                 yield event
 
 
-INCLUDE = MarkupTemplate.INCLUDE
+EXEC = MarkupTemplate.EXEC
--- a/genshi/template/plugin.py
+++ b/genshi/template/plugin.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # Copyright (C) 2006 Matthew Good
 # All rights reserved.
 #
@@ -20,17 +20,17 @@
 
 from genshi.input import ET, HTML, XML
 from genshi.output import DocType
-from genshi.template.core import Context, Template
-from genshi.template.eval import Undefined
+from genshi.template.base import Template
 from genshi.template.loader import TemplateLoader
 from genshi.template.markup import MarkupTemplate
 from genshi.template.text import TextTemplate
 
-__all__ = ['ConfigurationError', 'MarkupTemplateEnginePlugin',
-           'TextTemplateEnginePlugin']
+__all__ = ['ConfigurationError', 'AbstractTemplateEnginePlugin',
+           'MarkupTemplateEnginePlugin', 'TextTemplateEnginePlugin']
+__docformat__ = 'restructuredtext en'
 
 
-class ConfigurationError(Exception):
+class ConfigurationError(ValueError):
     """Exception raised when invalid plugin options are encountered."""
 
 
@@ -50,17 +50,24 @@
         auto_reload = options.get('genshi.auto_reload', '1')
         if isinstance(auto_reload, basestring):
             auto_reload = auto_reload.lower() in ('1', 'on', 'yes', 'true')
-        search_path = options.get('genshi.search_path', '').split(':')
+        search_path = filter(None, options.get('genshi.search_path', '').split(':'))
+        self.use_package_naming = not search_path
         try:
             max_cache_size = int(options.get('genshi.max_cache_size', 25))
         except ValueError:
             raise ConfigurationError('Invalid value for max_cache_size: "%s"' %
                                      options.get('genshi.max_cache_size'))
 
+        lookup_errors = options.get('genshi.lookup_errors', 'lenient')
+        if lookup_errors not in ('lenient', 'strict'):
+            raise ConfigurationError('Unknown lookup errors mode "%s"' %
+                                     lookup_errors)
+
         self.loader = TemplateLoader(filter(None, search_path),
                                      auto_reload=auto_reload,
                                      max_cache_size=max_cache_size,
-                                     default_class=self.template_class)
+                                     default_class=self.template_class,
+                                     variable_lookup=lookup_errors)
 
     def load_template(self, templatename, template_string=None):
         """Find a template specified in python 'dot' notation, or load one from
@@ -69,11 +76,12 @@
         if template_string is not None:
             return self.template_class(template_string)
 
-        divider = templatename.rfind('.')
-        if divider >= 0:
-            package = templatename[:divider]
-            basename = templatename[divider + 1:] + self.extension
-            templatename = resource_filename(package, basename)
+        if self.use_package_naming:
+            divider = templatename.rfind('.')
+            if divider >= 0:
+                package = templatename[:divider]
+                basename = templatename[divider + 1:] + self.extension
+                templatename = resource_filename(package, basename)
 
         return self.loader.load(templatename)
 
@@ -94,17 +102,7 @@
         """Render the output to an event stream."""
         if not isinstance(template, Template):
             template = self.load_template(template)
-        ctxt = Context(**info)
-
-        # Some functions for Kid compatibility
-        def defined(name):
-            return ctxt.get(name, Undefined) is not Undefined
-        ctxt['defined'] = defined
-        def value_of(name, default=None):
-            return ctxt.get(name, default)
-        ctxt['value_of'] = value_of
-
-        return template.generate(ctxt)
+        return template.generate(**info)
 
 
 class MarkupTemplateEnginePlugin(AbstractTemplateEnginePlugin):
@@ -113,22 +111,21 @@
     template_class = MarkupTemplate
     extension = '.html'
 
-    doctypes = {'html': DocType.HTML, 'html-strict': DocType.HTML_STRICT,
-                'html-transitional': DocType.HTML_TRANSITIONAL,
-                'xhtml': DocType.XHTML, 'xhtml-strict': DocType.XHTML_STRICT,
-                'xhtml-transitional': DocType.XHTML_TRANSITIONAL}
-
     def __init__(self, extra_vars_func=None, options=None):
         AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
 
-        doctype = self.options.get('genshi.default_doctype')
-        if doctype and doctype not in self.doctypes:
-            raise ConfigurationError('Unknown doctype "%s"' % doctype)
-        self.default_doctype = self.doctypes.get(doctype)
+        default_doctype = self.options.get('genshi.default_doctype')
+        if default_doctype:
+            doctype = DocType.get(default_doctype)
+            if doctype is None:
+                raise ConfigurationError('Unknown doctype %r' % default_doctype)
+            self.default_doctype = doctype
+        else:
+            self.default_doctype = None
 
-        format = self.options.get('genshi.default_format', 'html')
+        format = self.options.get('genshi.default_format', 'html').lower()
         if format not in ('html', 'xhtml', 'xml', 'text'):
-            raise ConfigurationError('Unknown output format "%s"' % format)
+            raise ConfigurationError('Unknown output format %r' % format)
         self.default_format = format
 
     def _get_render_options(self, format=None):
--- a/genshi/template/tests/__init__.py
+++ b/genshi/template/tests/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -14,14 +14,14 @@
 import doctest
 import unittest
 
-
 def suite():
-    from genshi.template.tests import core, directives, eval, loader, markup, \
-                                      plugin, text
+    from genshi.template.tests import base, directives, eval, interpolation, \
+                                      loader, markup, plugin, text
     suite = unittest.TestSuite()
-    suite.addTest(core.suite())
+    suite.addTest(base.suite())
     suite.addTest(directives.suite())
     suite.addTest(eval.suite())
+    suite.addTest(interpolation.suite())
     suite.addTest(loader.suite())
     suite.addTest(markup.suite())
     suite.addTest(plugin.suite())
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/base.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2007 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from genshi.template.base import Template
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(Template.__module__))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
deleted file mode 100644
--- a/genshi/template/tests/core.py
+++ /dev/null
@@ -1,114 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
-
-import doctest
-import unittest
-
-from genshi.core import Stream
-from genshi.template.core import Template
-
-
-class TemplateTestCase(unittest.TestCase):
-    """Tests for basic template processing, expression evaluation and error
-    reporting.
-    """
-
-    def test_interpolate_string(self):
-        parts = list(Template._interpolate('bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('bla', parts[0][1])
-
-    def test_interpolate_simple(self):
-        parts = list(Template._interpolate('${bla}'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('bla', parts[0][1].source)
-
-    def test_interpolate_escaped(self):
-        parts = list(Template._interpolate('$${bla}'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('${bla}', parts[0][1])
-
-    def test_interpolate_short(self):
-        parts = list(Template._interpolate('$bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('bla', parts[0][1].source)
-
-    def test_interpolate_short_starting_with_underscore(self):
-        parts = list(Template._interpolate('$_bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('_bla', parts[0][1].source)
-
-    def test_interpolate_short_containing_underscore(self):
-        parts = list(Template._interpolate('$foo_bar'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('foo_bar', parts[0][1].source)
-
-    def test_interpolate_short_starting_with_dot(self):
-        parts = list(Template._interpolate('$.bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('$.bla', parts[0][1])
-
-    def test_interpolate_short_containing_dot(self):
-        parts = list(Template._interpolate('$foo.bar'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('foo.bar', parts[0][1].source)
-
-    def test_interpolate_short_starting_with_digit(self):
-        parts = list(Template._interpolate('$0bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('$0bla', parts[0][1])
-
-    def test_interpolate_short_containing_digit(self):
-        parts = list(Template._interpolate('$foo0'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('foo0', parts[0][1].source)
-
-    def test_interpolate_mixed1(self):
-        parts = list(Template._interpolate('$foo bar $baz'))
-        self.assertEqual(3, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('foo', parts[0][1].source)
-        self.assertEqual(Stream.TEXT, parts[1][0])
-        self.assertEqual(' bar ', parts[1][1])
-        self.assertEqual(Template.EXPR, parts[2][0])
-        self.assertEqual('baz', parts[2][1].source)
-
-    def test_interpolate_mixed2(self):
-        parts = list(Template._interpolate('foo $bar baz'))
-        self.assertEqual(3, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('foo ', parts[0][1])
-        self.assertEqual(Template.EXPR, parts[1][0])
-        self.assertEqual('bar', parts[1][1].source)
-        self.assertEqual(Stream.TEXT, parts[2][0])
-        self.assertEqual(' baz', parts[2][1])
-
-
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(Template.__module__))
-    suite.addTest(unittest.makeSuite(TemplateTestCase, 'test'))
-    return suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
--- a/genshi/template/tests/directives.py
+++ b/genshi/template/tests/directives.py
@@ -16,7 +16,7 @@
 import unittest
 
 from genshi.template import directives, MarkupTemplate, TextTemplate, \
-                            TemplateRuntimeError
+                            TemplateRuntimeError, TemplateSyntaxError
 
 
 class AttrsDirectiveTestCase(unittest.TestCase):
@@ -384,6 +384,25 @@
                       Hi, you!
         """, str(tmpl.generate()))
 
+    def test_function_with_star_args(self):
+        """
+        Verify that a named template function using "star arguments" works as
+        expected.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:def="f(*args, **kwargs)">
+            ${repr(args)}
+            ${repr(kwargs)}
+          </div>
+          ${f(1, 2, a=3, b=4)}
+        </doc>""")
+        self.assertEqual("""<doc>
+          <div>
+            [1, 2]
+            {'a': 3, 'b': 4}
+          </div>
+        </doc>""", str(tmpl.generate()))
+
 
 class ForDirectiveTestCase(unittest.TestCase):
     """Tests for the `py:for` template directive."""
@@ -462,6 +481,7 @@
         </doc>""", filename='test.html')
         try:
             list(tmpl.generate(foo=12))
+            self.fail('Expected TemplateRuntimeError')
         except TemplateRuntimeError, e:
             self.assertEqual('test.html', e.filename)
             if sys.version_info[:2] >= (2, 4):
@@ -837,6 +857,25 @@
     #    </div>""", str(tmpl.generate()))
 
 
+class ReplaceDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:replace` template directive."""
+
+    def test_replace_with_empty_value(self):
+        """
+        Verify that the directive raises an apprioriate exception when an empty
+        expression is supplied.
+        """
+        try:
+            tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+              <elem py:replace="">Foo</elem>
+            </doc>""", filename='test.html')
+            self.fail('Expected TemplateSyntaxError')
+        except TemplateSyntaxError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(2, e.lineno)
+
+
 class StripDirectiveTestCase(unittest.TestCase):
     """Tests for the `py:strip` template directive."""
 
@@ -951,6 +990,7 @@
     suite.addTest(unittest.makeSuite(ForDirectiveTestCase, 'test'))
     suite.addTest(unittest.makeSuite(IfDirectiveTestCase, 'test'))
     suite.addTest(unittest.makeSuite(MatchDirectiveTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(ReplaceDirectiveTestCase, 'test'))
     suite.addTest(unittest.makeSuite(StripDirectiveTestCase, 'test'))
     suite.addTest(unittest.makeSuite(WithDirectiveTestCase, 'test'))
     return suite
--- a/genshi/template/tests/eval.py
+++ b/genshi/template/tests/eval.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -15,7 +15,9 @@
 import sys
 import unittest
 
-from genshi.template.eval import Expression, Undefined
+from genshi.core import Markup
+from genshi.template.eval import Expression, Suite, Undefined, UndefinedError, \
+                                 UNDEFINED
 
 
 class ExpressionTestCase(unittest.TestCase):
@@ -36,6 +38,10 @@
         self.assertEqual('bar', Expression('id').evaluate({'id': 'bar'}))
         self.assertEqual(None, Expression('id').evaluate({'id': None}))
 
+    def test_builtins(self):
+        expr = Expression('Markup')
+        self.assertEqual(expr.evaluate({}), Markup)
+
     def test_str_literal(self):
         self.assertEqual('foo', Expression('"foo"').evaluate({}))
         self.assertEqual('foo', Expression('"""foo"""').evaluate({}))
@@ -314,88 +320,263 @@
         expr = Expression("numbers[:-1]")
         self.assertEqual([0, 1, 2, 3], expr.evaluate({'numbers': range(5)}))
 
-    def test_error_access_undefined(self):
+    def test_access_undefined(self):
         expr = Expression("nothing", filename='index.html', lineno=50)
-        self.assertEqual(Undefined, type(expr.evaluate({})))
+        retval = expr.evaluate({})
+        assert isinstance(retval, Undefined)
+        self.assertEqual('nothing', retval._name)
+        assert retval._owner is UNDEFINED
 
-    def test_error_call_undefined(self):
-        expr = Expression("nothing()", filename='index.html', lineno=50)
+    def test_getattr_undefined(self):
+        class Something(object):
+            def __repr__(self):
+                return '<Something>'
+        something = Something()
+        expr = Expression('something.nil', filename='index.html', lineno=50)
+        retval = expr.evaluate({'something': something})
+        assert isinstance(retval, Undefined)
+        self.assertEqual('nil', retval._name)
+        assert retval._owner is something
+
+    def test_getitem_undefined_string(self):
+        class Something(object):
+            def __repr__(self):
+                return '<Something>'
+        something = Something()
+        expr = Expression('something["nil"]', filename='index.html', lineno=50)
+        retval = expr.evaluate({'something': something})
+        assert isinstance(retval, Undefined)
+        self.assertEqual('nil', retval._name)
+        assert retval._owner is something
+
+    def test_error_access_undefined(self):
+        expr = Expression("nothing", filename='index.html', lineno=50,
+                          lookup='strict')
         try:
             expr.evaluate({})
-            self.fail('Expected NameError')
-        except NameError, e:
-            exc_type, exc_value, exc_traceback = sys.exc_info()
-            frame = exc_traceback.tb_next
-            frames = []
-            while frame.tb_next:
-                frame = frame.tb_next
-                frames.append(frame)
-            self.assertEqual('Variable "nothing" is not defined', str(e))
-            self.assertEqual('<Expression "nothing()">',
-                             frames[-3].tb_frame.f_code.co_name)
-            self.assertEqual('index.html',
-                             frames[-3].tb_frame.f_code.co_filename)
-            self.assertEqual(50, frames[-3].tb_lineno)
-
-    def test_error_getattr_undefined(self):
-        expr = Expression("nothing.nil", filename='index.html', lineno=50)
-        try:
-            expr.evaluate({})
-            self.fail('Expected NameError')
-        except NameError, e:
+            self.fail('Expected UndefinedError')
+        except UndefinedError, e:
             exc_type, exc_value, exc_traceback = sys.exc_info()
             frame = exc_traceback.tb_next
             frames = []
             while frame.tb_next:
                 frame = frame.tb_next
                 frames.append(frame)
-            self.assertEqual('Variable "nothing" is not defined', str(e))
-            self.assertEqual('<Expression "nothing.nil">',
+            self.assertEqual('"nothing" not defined', str(e))
+            self.assertEqual("<Expression 'nothing'>",
                              frames[-3].tb_frame.f_code.co_name)
             self.assertEqual('index.html',
                              frames[-3].tb_frame.f_code.co_filename)
             self.assertEqual(50, frames[-3].tb_lineno)
 
-    def test_error_getitem_undefined(self):
-        expr = Expression("nothing[0]", filename='index.html', lineno=50)
+    def test_error_getattr_undefined(self):
+        class Something(object):
+            def __repr__(self):
+                return '<Something>'
+        expr = Expression('something.nil', filename='index.html', lineno=50,
+                          lookup='strict')
         try:
-            expr.evaluate({})
-            self.fail('Expected NameError')
-        except NameError, e:
+            expr.evaluate({'something': Something()})
+            self.fail('Expected UndefinedError')
+        except UndefinedError, e:
+            self.assertEqual('<Something> has no member named "nil"', str(e))
             exc_type, exc_value, exc_traceback = sys.exc_info()
+            search_string = "<Expression 'something.nil'>"
             frame = exc_traceback.tb_next
-            frames = []
             while frame.tb_next:
                 frame = frame.tb_next
-                frames.append(frame)
-            self.assertEqual('Variable "nothing" is not defined', str(e))
-            self.assertEqual('<Expression "nothing[0]">',
-                             frames[-3].tb_frame.f_code.co_name)
-            self.assertEqual('index.html',
-                             frames[-3].tb_frame.f_code.co_filename)
-            self.assertEqual(50, frames[-3].tb_lineno)
+                code = frame.tb_frame.f_code
+                if code.co_name == search_string:
+                    break
+            else:
+                self.fail("never found the frame I was looking for")
+            self.assertEqual('index.html', code.co_filename)
+            self.assertEqual(50, frame.tb_lineno)
 
-    def test_error_getattr_nested_undefined(self):
-        expr = Expression("nothing.nil", filename='index.html', lineno=50)
-        val = expr.evaluate({'nothing': object()})
-        assert isinstance(val, Undefined)
-        self.assertEqual("nil", val._name)
+    def test_error_getitem_undefined_string(self):
+        class Something(object):
+            def __repr__(self):
+                return '<Something>'
+        expr = Expression('something["nil"]', filename='index.html', lineno=50,
+                          lookup='strict')
+        try:
+            expr.evaluate({'something': Something()})
+            self.fail('Expected UndefinedError')
+        except UndefinedError, e:
+            self.assertEqual('<Something> has no member named "nil"', str(e))
+            exc_type, exc_value, exc_traceback = sys.exc_info()
+            search_string = '''<Expression 'something["nil"]'>'''
+            frame = exc_traceback.tb_next
+            while frame.tb_next:
+                frame = frame.tb_next
+                code = frame.tb_frame.f_code
+                if code.co_name == search_string:
+                    break
+            else:
+                self.fail("never found the frame I was looking for")
+            self.assertEqual('index.html', code.co_filename)
+            self.assertEqual(50, frame.tb_lineno)
 
-    def test_error_getitem_nested_undefined_string(self):
-        expr = Expression("nothing['bla']", filename='index.html', lineno=50)
-        val = expr.evaluate({'nothing': object()})
-        assert isinstance(val, Undefined)
-        self.assertEqual("bla", val._name)
 
-    def test_error_getitem_nested_undefined_int(self):
-        expr = Expression("nothing[0]", filename='index.html', lineno=50)
-        self.assertRaises(TypeError, expr.evaluate, {'nothing': object()})
+class SuiteTestCase(unittest.TestCase):
+
+    def test_assign(self):
+        suite = Suite("foo = 42")
+        data = {}
+        suite.execute(data)
+        self.assertEqual(42, data['foo'])
+
+    def test_def(self):
+        suite = Suite("def donothing(): pass")
+        data = {}
+        suite.execute(data)
+        assert 'donothing' in data
+        self.assertEqual(None, data['donothing']())
+
+    def test_def_with_multiple_statements(self):
+        suite = Suite("""def donothing():
+    if True:
+        return foo
+""")
+        data = {'foo': 'bar'}
+        suite.execute(data)
+        assert 'donothing' in data
+        self.assertEqual('bar', data['donothing']())
+
+    def test_delete(self):
+        suite = Suite("""foo = 42
+del foo
+""")
+        data = {}
+        suite.execute(data)
+        assert 'foo' not in data
+
+    def test_class(self):
+        suite = Suite("class plain(object): pass")
+        data = {}
+        suite.execute(data)
+        assert 'plain' in data
+
+    def test_import(self):
+        suite = Suite("from itertools import ifilter")
+        data = {}
+        suite.execute(data)
+        assert 'ifilter' in data
+
+    def test_for(self):
+        suite = Suite("""x = []
+for i in range(3):
+    x.append(i**2)
+""")
+        data = {}
+        suite.execute(data)
+        self.assertEqual([0, 1, 4], data['x'])
+
+    def test_if(self):
+        suite = Suite("""if foo == 42:
+    x = True
+""")
+        data = {'foo': 42}
+        suite.execute(data)
+        self.assertEqual(True, data['x'])
+
+    def test_raise(self):
+        suite = Suite("""raise NotImplementedError""")
+        self.assertRaises(NotImplementedError, suite.execute, {})
+
+    def test_try_except(self):
+        suite = Suite("""try:
+    import somemod
+except ImportError:
+    somemod = None
+else:
+    somemod.dosth()""")
+        data = {}
+        suite.execute(data)
+        self.assertEqual(None, data['somemod'])
+
+    def test_finally(self):
+        suite = Suite("""try:
+    x = 2
+finally:
+    x = None
+""")
+        data = {}
+        suite.execute(data)
+        self.assertEqual(None, data['x'])
+
+    def test_while_break(self):
+        suite = Suite("""x = 0
+while x < 5:
+    x += step
+    if x == 4:
+        break
+""")
+        data = {'step': 2}
+        suite.execute(data)
+        self.assertEqual(4, data['x'])
+
+    def test_augmented_attribute_assignment(self):
+        suite = Suite("d['k'] += 42")
+        d = {"k": 1}
+        suite.execute({"d": d})
+        self.assertEqual(43, d["k"])
+
+    def test_local_augmented_assign(self):
+        Suite("x = 1; x += 42; assert x == 43").execute({})
+
+    def test_assign_in_list(self):
+        suite = Suite("[d['k']] = 'foo',; assert d['k'] == 'foo'")
+        d = {"k": "bar"}
+        suite.execute({"d": d})
+        self.assertEqual("foo", d["k"])
+
+    def test_exec(self):
+        suite = Suite("x = 1; exec d['k']; assert x == 42, x")
+        suite.execute({"d": {"k": "x = 42"}})
+
+    def test_return(self):
+        suite = Suite("""
+def f():
+    return v
+
+assert f() == 42
+""")
+        suite.execute({"v": 42})
+
+    def test_assign_to_dict_item(self):
+        suite = Suite("d['k'] = 'foo'")
+        data = {'d': {}}
+        suite.execute(data)
+        self.assertEqual('foo', data['d']['k'])
+
+    def test_assign_to_attribute(self):
+        class Something(object): pass
+        something = Something()
+        suite = Suite("obj.attr = 'foo'")
+        data = {"obj": something}
+        suite.execute(data)
+        self.assertEqual('foo', something.attr)
+
+    def test_delattr(self):
+        class Something(object):
+            def __init__(self):
+                self.attr = 'foo'
+        obj = Something()
+        Suite("del obj.attr").execute({'obj': obj})
+        self.failIf(hasattr(obj, 'attr'))
+
+    def test_delitem(self):
+        d = {'k': 'foo'}
+        Suite("del d['k']").execute({'d': d})
+        self.failIf('k' in d, `d`)
 
 
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(Expression.__module__))
     suite.addTest(unittest.makeSuite(ExpressionTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(SuiteTestCase, 'test'))
     return suite
 
 if __name__ == '__main__':
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/interpolation.py
@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
+
+import doctest
+import sys
+import unittest
+
+from genshi.core import TEXT
+from genshi.template.base import TemplateSyntaxError, EXPR
+from genshi.template.interpolation import interpolate
+
+
+class InterpolateTestCase(unittest.TestCase):
+
+    def test_interpolate_string(self):
+        parts = list(interpolate('bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('bla', parts[0][1])
+
+    def test_interpolate_simple(self):
+        parts = list(interpolate('${bla}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('bla', parts[0][1].source)
+
+    def test_interpolate_escaped(self):
+        parts = list(interpolate('$${bla}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('${bla}', parts[0][1])
+
+    def test_interpolate_dobuleescaped(self):
+        parts = list(interpolate('$$${bla}'))
+        self.assertEqual(2, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('$', parts[0][1])
+        self.assertEqual(EXPR, parts[1][0])
+        self.assertEqual('bla', parts[1][1].source)
+
+    def test_interpolate_short(self):
+        parts = list(interpolate('$bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('bla', parts[0][1].source)
+
+    def test_interpolate_short_escaped(self):
+        parts = list(interpolate('$$bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('$bla', parts[0][1])
+
+    def test_interpolate_short_doubleescaped(self):
+        parts = list(interpolate('$$$bla'))
+        self.assertEqual(2, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('$', parts[0][1])
+        self.assertEqual(EXPR, parts[1][0])
+        self.assertEqual('bla', parts[1][1].source)
+
+    def test_interpolate_short_starting_with_underscore(self):
+        parts = list(interpolate('$_bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('_bla', parts[0][1].source)
+
+    def test_interpolate_short_containing_underscore(self):
+        parts = list(interpolate('$foo_bar'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('foo_bar', parts[0][1].source)
+
+    def test_interpolate_short_starting_with_dot(self):
+        parts = list(interpolate('$.bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('$.bla', parts[0][1])
+
+    def test_interpolate_short_containing_dot(self):
+        parts = list(interpolate('$foo.bar'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('foo.bar', parts[0][1].source)
+
+    def test_interpolate_short_starting_with_digit(self):
+        parts = list(interpolate('$0bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('$0bla', parts[0][1])
+
+    def test_interpolate_short_containing_digit(self):
+        parts = list(interpolate('$foo0'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('foo0', parts[0][1].source)
+
+    def test_interpolate_short_starting_with_digit(self):
+        parts = list(interpolate('$0bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('$0bla', parts[0][1])
+
+    def test_interpolate_short_containing_digit(self):
+        parts = list(interpolate('$foo0'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('foo0', parts[0][1].source)
+
+    def test_interpolate_full_nested_brackets(self):
+        parts = list(interpolate('${{1:2}}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('{1:2}', parts[0][1].source)
+
+    def test_interpolate_full_mismatched_brackets(self):
+        try:
+            list(interpolate('${{1:2}'))
+        except TemplateSyntaxError, e:
+            pass
+        else:
+            self.fail('Expected TemplateSyntaxError')
+
+    def test_interpolate_quoted_brackets_1(self):
+        parts = list(interpolate('${"}"}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('"}"', parts[0][1].source)
+
+    def test_interpolate_quoted_brackets_2(self):
+        parts = list(interpolate("${'}'}"))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual("'}'", parts[0][1].source)
+
+    def test_interpolate_quoted_brackets_3(self):
+        parts = list(interpolate("${'''}'''}"))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual("'''}'''", parts[0][1].source)
+
+    def test_interpolate_quoted_brackets_4(self):
+        parts = list(interpolate("${'''}\"\"\"'''}"))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual("'''}\"\"\"'''", parts[0][1].source)
+
+    def test_interpolate_quoted_brackets_5(self):
+        parts = list(interpolate(r"${'\'}'}"))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual(r"'\'}'", parts[0][1].source)
+
+    def test_interpolate_mixed1(self):
+        parts = list(interpolate('$foo bar $baz'))
+        self.assertEqual(3, len(parts))
+        self.assertEqual(EXPR, parts[0][0])
+        self.assertEqual('foo', parts[0][1].source)
+        self.assertEqual(TEXT, parts[1][0])
+        self.assertEqual(' bar ', parts[1][1])
+        self.assertEqual(EXPR, parts[2][0])
+        self.assertEqual('baz', parts[2][1].source)
+
+    def test_interpolate_mixed2(self):
+        parts = list(interpolate('foo $bar baz'))
+        self.assertEqual(3, len(parts))
+        self.assertEqual(TEXT, parts[0][0])
+        self.assertEqual('foo ', parts[0][1])
+        self.assertEqual(EXPR, parts[1][0])
+        self.assertEqual('bar', parts[1][1].source)
+        self.assertEqual(TEXT, parts[2][0])
+        self.assertEqual(' baz', parts[2][1])
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(interpolate.__module__))
+    suite.addTest(unittest.makeSuite(InterpolateTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
--- a/genshi/template/tests/loader.py
+++ b/genshi/template/tests/loader.py
@@ -17,6 +17,7 @@
 import tempfile
 import unittest
 
+from genshi.core import TEXT
 from genshi.template.loader import TemplateLoader
 from genshi.template.markup import MarkupTemplate
 
@@ -189,6 +190,35 @@
         loader = TemplateLoader([self.dirname], default_encoding='utf-8')
         loader.load('tmpl.html', encoding='iso-8859-1')
 
+    def test_load_with_callback(self):
+        fileobj = open(os.path.join(self.dirname, 'tmpl.html'), 'w')
+        try:
+            fileobj.write("""<html>
+              <p>Hello</p>
+            </html>""")
+        finally:
+            fileobj.close()
+
+        def template_loaded(template):
+            def my_filter(stream, ctxt):
+                for kind, data, pos in stream:
+                    if kind is TEXT and data.strip():
+                        data = ', '.join([data, data.lower()])
+                    yield kind, data, pos
+            template.filters.insert(0, my_filter)
+
+        loader = TemplateLoader([self.dirname], callback=template_loaded)
+        tmpl = loader.load('tmpl.html')
+        self.assertEqual("""<html>
+              <p>Hello, hello</p>
+            </html>""", tmpl.generate().render())
+
+        # Make sure the filter is only added once
+        tmpl = loader.load('tmpl.html')
+        self.assertEqual("""<html>
+              <p>Hello, hello</p>
+            </html>""", tmpl.generate().render())
+
 
 def suite():
     suite = unittest.TestSuite()
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -21,7 +21,7 @@
 
 from genshi.core import Markup
 from genshi.input import XML
-from genshi.template.core import BadDirectiveError, TemplateSyntaxError
+from genshi.template.base import BadDirectiveError, TemplateSyntaxError
 from genshi.template.loader import TemplateLoader
 from genshi.template.markup import MarkupTemplate
 
@@ -183,7 +183,7 @@
         <div xmlns:py="http://genshi.edgewall.org/">
           \xf6
         </div>""".encode('iso-8859-1'), encoding='iso-8859-1')
-        self.assertEqual(u"""<div>
+        self.assertEqual(u"""<?xml version="1.0" encoding="iso-8859-1"?>\n<div>
           \xf6
         </div>""", unicode(tmpl.generate()))
 
@@ -195,6 +195,44 @@
           \xf6
         </div>""", unicode(tmpl.generate()))
 
+    def test_exec_import(self):
+        tmpl = MarkupTemplate(u"""<?python from datetime import timedelta ?>
+        <div xmlns:py="http://genshi.edgewall.org/">
+          ${timedelta(days=2)}
+        </div>""")
+        self.assertEqual(u"""<div>
+          2 days, 0:00:00
+        </div>""", str(tmpl.generate()))
+
+    def test_exec_def(self):
+        tmpl = MarkupTemplate(u"""
+        <?python
+        def foo():
+            return 42
+        ?>
+        <div xmlns:py="http://genshi.edgewall.org/">
+          ${foo()}
+        </div>""")
+        self.assertEqual(u"""<div>
+          42
+        </div>""", str(tmpl.generate()))
+
+    def test_namespace_on_removed_elem(self):
+        """
+        Verify that a namespace declaration on an element that is removed from
+        the generated stream does not get pushed up to the next non-stripped
+        element (see ticket #107).
+        """
+        tmpl = MarkupTemplate("""<?xml version="1.0"?>
+        <Test xmlns:py="http://genshi.edgewall.org/">
+          <Size py:if="0" xmlns:t="test">Size</Size>
+          <Item/>
+        </Test>""")
+        self.assertEqual("""<?xml version="1.0"?>\n<Test>
+          
+          <Item/>
+        </Test>""", str(tmpl.generate()))
+
     def test_include_in_loop(self):
         dirname = tempfile.mkdtemp(suffix='genshi_test')
         try:
--- a/genshi/template/tests/plugin.py
+++ b/genshi/template/tests/plugin.py
@@ -118,7 +118,7 @@
         output = plugin.render({'message': 'Hello'}, format='xhtml',
                                template=tmpl)
         self.assertEqual("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
   <head>
     <title>Test</title>
   </head>
--- a/genshi/template/tests/text.py
+++ b/genshi/template/tests/text.py
@@ -12,14 +12,24 @@
 # history and logs, available at http://genshi.edgewall.org/log/.
 
 import doctest
+import os
+import shutil
+import tempfile
 import unittest
 
+from genshi.template.loader import TemplateLoader
 from genshi.template.text import TextTemplate
 
 
 class TextTemplateTestCase(unittest.TestCase):
     """Tests for text template processing."""
 
+    def setUp(self):
+        self.dirname = tempfile.mkdtemp(suffix='markup_test')
+
+    def tearDown(self):
+        shutil.rmtree(self.dirname)
+
     def test_escaping(self):
         tmpl = TextTemplate('\\#escaped')
         self.assertEqual('#escaped', str(tmpl.generate()))
@@ -37,7 +47,7 @@
         #if foo
           bar
         #end 'if foo'""")
-        self.assertEqual('\n', str(tmpl.generate()))
+        self.assertEqual('\n', str(tmpl.generate(foo=False)))
 
     def test_latin1_encoded(self):
         text = u'$foo\xf6$bar'.encode('iso-8859-1')
@@ -74,7 +84,28 @@
 
 """, tmpl.generate(items=range(3)).render('text'))
 
+    def test_include(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w')
+        try:
+            file1.write("Included\n")
+        finally:
+            file1.close()
 
+        file2 = open(os.path.join(self.dirname, 'tmpl2.txt'), 'w')
+        try:
+            file2.write("""----- Included data below this line -----
+            #include tmpl1.txt
+            ----- Included data above this line -----""")
+        finally:
+            file2.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl = loader.load('tmpl2.txt', cls=TextTemplate)
+        self.assertEqual("""----- Included data below this line -----
+Included
+            ----- Included data above this line -----""",
+                         tmpl.generate().render())
+        
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(TextTemplate.__module__))
--- a/genshi/template/text.py
+++ b/genshi/template/text.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -15,8 +15,13 @@
 
 import re
 
-from genshi.template.core import BadDirectiveError, Template, SUB
+from genshi.template.base import BadDirectiveError, Template, INCLUDE, SUB
 from genshi.template.directives import *
+from genshi.template.directives import Directive, _apply_directives
+from genshi.template.interpolation import interpolate
+
+__all__ = ['TextTemplate']
+__docformat__ = 'restructuredtext en'
 
 
 class TextTemplate(Template):
@@ -70,8 +75,9 @@
             start, end = mo.span()
             if start > offset:
                 text = source[offset:start]
-                for kind, data, pos in self._interpolate(text, self.basedir,
-                                                         self.filename, lineno):
+                for kind, data, pos in interpolate(text, self.basedir,
+                                                   self.filename, lineno,
+                                                   lookup=self.lookup):
                     stream.append((kind, data, pos))
                 lineno += len(text.splitlines())
 
@@ -90,6 +96,9 @@
                     substream = stream[start_offset:]
                     stream[start_offset:] = [(SUB, ([directive], substream),
                                               (self.filepath, lineno, 0))]
+            elif command == 'include':
+                pos = (self.filename, lineno, 0)
+                stream.append((INCLUDE, (value.strip(), []), pos))
             elif command != '#':
                 cls = self._dir_by_name.get(command)
                 if cls is None:
@@ -102,8 +111,9 @@
 
         if offset < len(source):
             text = source[offset:].replace('\\#', '#')
-            for kind, data, pos in self._interpolate(text, self.basedir,
-                                                     self.filename, lineno):
+            for kind, data, pos in interpolate(text, self.basedir,
+                                               self.filename, lineno,
+                                               lookup=self.lookup):
                 stream.append((kind, data, pos))
 
         return stream
--- a/genshi/tests/__init__.py
+++ b/genshi/tests/__init__.py
@@ -15,8 +15,8 @@
 
 def suite():
     import genshi
-    from genshi.tests import builder, core, filters, input, output, path, \
-                             util
+    from genshi.tests import builder, core, input, output, path, util
+    from genshi.filters import tests as filters
     from genshi.template import tests as template
 
     suite = unittest.TestSuite()
deleted file mode 100644
--- a/genshi/tests/filters.py
+++ /dev/null
@@ -1,380 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# 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://genshi.edgewall.org/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://genshi.edgewall.org/log/.
-
-import doctest
-import unittest
-
-from genshi import filters
-from genshi.input import HTML, ParseError
-from genshi.filters import HTMLFormFiller, HTMLSanitizer
-
-
-class HTMLFormFillerTestCase(unittest.TestCase):
-
-    def test_fill_input_text_no_value(self):
-        html = HTML("""<form><p>
-          <input type="text" name="foo" />
-        </p></form>""") | HTMLFormFiller()
-        self.assertEquals("""<form><p>
-          <input type="text" name="foo"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_input_text_single_value(self):
-        html = HTML("""<form><p>
-          <input type="text" name="foo" />
-        </p></form>""") | HTMLFormFiller(data={'foo': 'bar'})
-        self.assertEquals("""<form><p>
-          <input type="text" name="foo" value="bar"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_input_text_multi_value(self):
-        html = HTML("""<form><p>
-          <input type="text" name="foo" />
-        </p></form>""") | HTMLFormFiller(data={'foo': ['bar']})
-        self.assertEquals("""<form><p>
-          <input type="text" name="foo" value="bar"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_input_hidden_no_value(self):
-        html = HTML("""<form><p>
-          <input type="hidden" name="foo" />
-        </p></form>""") | HTMLFormFiller()
-        self.assertEquals("""<form><p>
-          <input type="hidden" name="foo"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_input_hidden_single_value(self):
-        html = HTML("""<form><p>
-          <input type="hidden" name="foo" />
-        </p></form>""") | HTMLFormFiller(data={'foo': 'bar'})
-        self.assertEquals("""<form><p>
-          <input type="hidden" name="foo" value="bar"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_input_hidden_multi_value(self):
-        html = HTML("""<form><p>
-          <input type="hidden" name="foo" />
-        </p></form>""") | HTMLFormFiller(data={'foo': ['bar']})
-        self.assertEquals("""<form><p>
-          <input type="hidden" name="foo" value="bar"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_textarea_no_value(self):
-        html = HTML("""<form><p>
-          <textarea name="foo"></textarea>
-        </p></form>""") | HTMLFormFiller()
-        self.assertEquals("""<form><p>
-          <textarea name="foo"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_textarea_single_value(self):
-        html = HTML("""<form><p>
-          <textarea name="foo"></textarea>
-        </p></form>""") | HTMLFormFiller(data={'foo': 'bar'})
-        self.assertEquals("""<form><p>
-          <textarea name="foo">bar</textarea>
-        </p></form>""", unicode(html))
-
-    def test_fill_textarea_multi_value(self):
-        html = HTML("""<form><p>
-          <textarea name="foo"></textarea>
-        </p></form>""") | HTMLFormFiller(data={'foo': ['bar']})
-        self.assertEquals("""<form><p>
-          <textarea name="foo">bar</textarea>
-        </p></form>""", unicode(html))
-
-    def test_fill_input_checkbox_no_value(self):
-        html = HTML("""<form><p>
-          <input type="checkbox" name="foo" />
-        </p></form>""") | HTMLFormFiller()
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_input_checkbox_single_value_auto(self):
-        html = HTML("""<form><p>
-          <input type="checkbox" name="foo" />
-        </p></form>""")
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ''})))
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo" checked="checked"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': 'on'})))
-
-    def test_fill_input_checkbox_single_value_defined(self):
-        html = HTML("""<form><p>
-          <input type="checkbox" name="foo" value="1" />
-        </p></form>""")
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo" value="1" checked="checked"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': '1'})))
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo" value="1"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': '2'})))
-
-    def test_fill_input_checkbox_multi_value_auto(self):
-        html = HTML("""<form><p>
-          <input type="checkbox" name="foo" />
-        </p></form>""")
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': []})))
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo" checked="checked"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['on']})))
-
-    def test_fill_input_checkbox_multi_value_defined(self):
-        html = HTML("""<form><p>
-          <input type="checkbox" name="foo" value="1" />
-        </p></form>""")
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo" value="1" checked="checked"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['1']})))
-        self.assertEquals("""<form><p>
-          <input type="checkbox" name="foo" value="1"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['2']})))
-
-    def test_fill_input_radio_no_value(self):
-        html = HTML("""<form><p>
-          <input type="radio" name="foo" />
-        </p></form>""") | HTMLFormFiller()
-        self.assertEquals("""<form><p>
-          <input type="radio" name="foo"/>
-        </p></form>""", unicode(html))
-
-    def test_fill_input_radio_single_value(self):
-        html = HTML("""<form><p>
-          <input type="radio" name="foo" value="1" />
-        </p></form>""")
-        self.assertEquals("""<form><p>
-          <input type="radio" name="foo" value="1" checked="checked"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': '1'})))
-        self.assertEquals("""<form><p>
-          <input type="radio" name="foo" value="1"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': '2'})))
-
-    def test_fill_input_radio_multi_value(self):
-        html = HTML("""<form><p>
-          <input type="radio" name="foo" value="1" />
-        </p></form>""")
-        self.assertEquals("""<form><p>
-          <input type="radio" name="foo" value="1" checked="checked"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['1']})))
-        self.assertEquals("""<form><p>
-          <input type="radio" name="foo" value="1"/>
-        </p></form>""", unicode(html | HTMLFormFiller(data={'foo': ['2']})))
-
-    def test_fill_select_no_value_auto(self):
-        html = HTML("""<form><p>
-          <select name="foo">
-            <option>1</option>
-            <option>2</option>
-            <option>3</option>
-          </select>
-        </p></form>""") | HTMLFormFiller()
-        self.assertEquals("""<form><p>
-          <select name="foo">
-            <option>1</option>
-            <option>2</option>
-            <option>3</option>
-          </select>
-        </p></form>""", unicode(html))
-
-    def test_fill_select_no_value_defined(self):
-        html = HTML("""<form><p>
-          <select name="foo">
-            <option value="1">1</option>
-            <option value="2">2</option>
-            <option value="3">3</option>
-          </select>
-        </p></form>""") | HTMLFormFiller()
-        self.assertEquals("""<form><p>
-          <select name="foo">
-            <option value="1">1</option>
-            <option value="2">2</option>
-            <option value="3">3</option>
-          </select>
-        </p></form>""", unicode(html))
-
-    def test_fill_select_single_value_auto(self):
-        html = HTML("""<form><p>
-          <select name="foo">
-            <option>1</option>
-            <option>2</option>
-            <option>3</option>
-          </select>
-        </p></form>""") | HTMLFormFiller(data={'foo': '1'})
-        self.assertEquals("""<form><p>
-          <select name="foo">
-            <option selected="selected">1</option>
-            <option>2</option>
-            <option>3</option>
-          </select>
-        </p></form>""", unicode(html))
-
-    def test_fill_select_single_value_defined(self):
-        html = HTML("""<form><p>
-          <select name="foo">
-            <option value="1">1</option>
-            <option value="2">2</option>
-            <option value="3">3</option>
-          </select>
-        </p></form>""") | HTMLFormFiller(data={'foo': '1'})
-        self.assertEquals("""<form><p>
-          <select name="foo">
-            <option value="1" selected="selected">1</option>
-            <option value="2">2</option>
-            <option value="3">3</option>
-          </select>
-        </p></form>""", unicode(html))
-
-    def test_fill_select_multi_value_auto(self):
-        html = HTML("""<form><p>
-          <select name="foo" multiple>
-            <option>1</option>
-            <option>2</option>
-            <option>3</option>
-          </select>
-        </p></form>""") | HTMLFormFiller(data={'foo': ['1', '3']})
-        self.assertEquals("""<form><p>
-          <select name="foo" multiple="multiple">
-            <option selected="selected">1</option>
-            <option>2</option>
-            <option selected="selected">3</option>
-          </select>
-        </p></form>""", unicode(html))
-
-    def test_fill_select_multi_value_defined(self):
-        html = HTML("""<form><p>
-          <select name="foo" multiple>
-            <option value="1">1</option>
-            <option value="2">2</option>
-            <option value="3">3</option>
-          </select>
-        </p></form>""") | HTMLFormFiller(data={'foo': ['1', '3']})
-        self.assertEquals("""<form><p>
-          <select name="foo" multiple="multiple">
-            <option value="1" selected="selected">1</option>
-            <option value="2">2</option>
-            <option value="3" selected="selected">3</option>
-          </select>
-        </p></form>""", unicode(html))
-
-
-class HTMLSanitizerTestCase(unittest.TestCase):
-
-    def test_sanitize_unchanged(self):
-        html = HTML('<a href="#">fo<br />o</a>')
-        self.assertEquals(u'<a href="#">fo<br/>o</a>',
-                          unicode(html | HTMLSanitizer()))
-
-    def test_sanitize_escape_text(self):
-        html = HTML('<a href="#">fo&amp;</a>')
-        self.assertEquals(u'<a href="#">fo&amp;</a>',
-                          unicode(html | HTMLSanitizer()))
-        html = HTML('<a href="#">&lt;foo&gt;</a>')
-        self.assertEquals(u'<a href="#">&lt;foo&gt;</a>',
-                          unicode(html | HTMLSanitizer()))
-
-    def test_sanitize_entityref_text(self):
-        html = HTML('<a href="#">fo&ouml;</a>')
-        self.assertEquals(u'<a href="#">foƶ</a>',
-                          unicode(html | HTMLSanitizer()))
-
-    def test_sanitize_escape_attr(self):
-        html = HTML('<div title="&lt;foo&gt;"></div>')
-        self.assertEquals(u'<div title="&lt;foo&gt;"/>',
-                          unicode(html | HTMLSanitizer()))
-
-    def test_sanitize_close_empty_tag(self):
-        html = HTML('<a href="#">fo<br>o</a>')
-        self.assertEquals(u'<a href="#">fo<br/>o</a>',
-                          unicode(html | HTMLSanitizer()))
-
-    def test_sanitize_invalid_entity(self):
-        html = HTML('&junk;')
-        self.assertEquals('&amp;junk;', unicode(html | HTMLSanitizer()))
-
-    def test_sanitize_remove_script_elem(self):
-        html = HTML('<script>alert("Foo")</script>')
-        self.assertEquals(u'', unicode(html | HTMLSanitizer()))
-        html = HTML('<SCRIPT SRC="http://example.com/"></SCRIPT>')
-        self.assertEquals(u'', unicode(html | HTMLSanitizer()))
-        self.assertRaises(ParseError, HTML, '<SCR\0IPT>alert("foo")</SCR\0IPT>')
-        self.assertRaises(ParseError, HTML,
-                          '<SCRIPT&XYZ SRC="http://example.com/"></SCRIPT>')
-
-    def test_sanitize_remove_onclick_attr(self):
-        html = HTML('<div onclick=\'alert("foo")\' />')
-        self.assertEquals(u'<div/>', unicode(html | HTMLSanitizer()))
-
-    def test_sanitize_remove_style_scripts(self):
-        # Inline style with url() using javascript: scheme
-        html = HTML('<DIV STYLE=\'background: url(javascript:alert("foo"))\'>')
-        self.assertEquals(u'<div/>', unicode(html | HTMLSanitizer()))
-        # Inline style with url() using javascript: scheme, using control char
-        html = HTML('<DIV STYLE=\'background: url(&#1;javascript:alert("foo"))\'>')
-        self.assertEquals(u'<div/>', unicode(html | HTMLSanitizer()))
-        # Inline style with url() using javascript: scheme, in quotes
-        html = HTML('<DIV STYLE=\'background: url("javascript:alert(foo)")\'>')
-        self.assertEquals(u'<div/>', unicode(html | HTMLSanitizer()))
-        # IE expressions in CSS not allowed
-        html = HTML('<DIV STYLE=\'width: expression(alert("foo"));\'>')
-        self.assertEquals(u'<div/>', unicode(html | HTMLSanitizer()))
-        html = HTML('<DIV STYLE=\'background: url(javascript:alert("foo"));'
-                                 'color: #fff\'>')
-        self.assertEquals(u'<div style="color: #fff"/>',
-                          unicode(html | HTMLSanitizer()))
-
-    def test_sanitize_remove_src_javascript(self):
-        html = HTML('<img src=\'javascript:alert("foo")\'>')
-        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
-        # Case-insensitive protocol matching
-        html = HTML('<IMG SRC=\'JaVaScRiPt:alert("foo")\'>')
-        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
-        # Grave accents (not parsed)
-        self.assertRaises(ParseError, HTML,
-                          '<IMG SRC=`javascript:alert("RSnake says, \'foo\'")`>')
-        # Protocol encoded using UTF-8 numeric entities
-        html = HTML('<IMG SRC=\'&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;'
-                    '&#112;&#116;&#58;alert("foo")\'>')
-        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
-        # Protocol encoded using UTF-8 numeric entities without a semicolon
-        # (which is allowed because the max number of digits is used)
-        html = HTML('<IMG SRC=\'&#0000106&#0000097&#0000118&#0000097'
-                    '&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116'
-                    '&#0000058alert("foo")\'>')
-        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
-        # Protocol encoded using UTF-8 numeric hex entities without a semicolon
-        # (which is allowed because the max number of digits is used)
-        html = HTML('<IMG SRC=\'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69'
-                    '&#x70&#x74&#x3A;alert("foo")\'>')
-        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
-        # Embedded tab character in protocol
-        html = HTML('<IMG SRC=\'jav\tascript:alert("foo");\'>')
-        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
-        # Embedded tab character in protocol, but encoded this time
-        html = HTML('<IMG SRC=\'jav&#x09;ascript:alert("foo");\'>')
-        self.assertEquals(u'<img/>', unicode(html | HTMLSanitizer()))
-
-
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(filters))
-    suite.addTest(unittest.makeSuite(HTMLFormFillerTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(HTMLSanitizerTestCase, 'test'))
-    return suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
--- a/genshi/tests/input.py
+++ b/genshi/tests/input.py
@@ -71,7 +71,7 @@
         <div>\xf6</div>
         """.encode('iso-8859-1')
         events = list(XMLParser(StringIO(text)))
-        kind, data, pos = events[1]
+        kind, data, pos = events[2]
         self.assertEqual(Stream.TEXT, kind)
         self.assertEqual(u'\xf6', data)
 
@@ -181,6 +181,33 @@
         self.assertEqual(u'php', target)
         self.assertEqual(u'echo "Foobar"', data)
 
+    def test_xmldecl(self):
+        text = '<?xml version="1.0" ?><root />'
+        events = list(XMLParser(StringIO(text)))
+        kind, (version, encoding, standalone), pos = events[0]
+        self.assertEqual(Stream.XML_DECL, kind)
+        self.assertEqual(u'1.0', version)
+        self.assertEqual(None, encoding)
+        self.assertEqual(-1, standalone)
+
+    def test_xmldecl_encoding(self):
+        text = '<?xml version="1.0" encoding="utf-8" ?><root />'
+        events = list(XMLParser(StringIO(text)))
+        kind, (version, encoding, standalone), pos = events[0]
+        self.assertEqual(Stream.XML_DECL, kind)
+        self.assertEqual(u'1.0', version)
+        self.assertEqual(u'utf-8', encoding)
+        self.assertEqual(-1, standalone)
+
+    def test_xmldecl_standalone(self):
+        text = '<?xml version="1.0" standalone="yes" ?><root />'
+        events = list(XMLParser(StringIO(text)))
+        kind, (version, encoding, standalone), pos = events[0]
+        self.assertEqual(Stream.XML_DECL, kind)
+        self.assertEqual(u'1.0', version)
+        self.assertEqual(None, encoding)
+        self.assertEqual(1, standalone)
+
     def test_processing_instruction_trailing_qmark(self):
         text = '<?php echo "Foobar" ??>'
         events = list(HTMLParser(StringIO(text)))
@@ -222,6 +249,14 @@
         self.assertEqual((Stream.END, 'b'), events[3][:2])
         self.assertEqual((Stream.END, 'span'), events[4][:2])
 
+    def test_hex_charref(self):
+        text = '<span>&#x27;</span>'
+        events = list(HTMLParser(StringIO(text)))
+        self.assertEqual(3, len(events))
+        self.assertEqual((Stream.START, ('span', ())), events[0][:2])
+        self.assertEqual((Stream.TEXT, "'"), events[1][:2])
+        self.assertEqual((Stream.END, 'span'), events[2][:2])
+
 
 def suite():
     suite = unittest.TestSuite()
--- a/genshi/tests/output.py
+++ b/genshi/tests/output.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -87,19 +87,19 @@
     def test_nested_default_namespaces(self):
         stream = Stream([
             (Stream.START_NS, ('', 'http://example.org/'), (None, -1, -1)),
-            (Stream.START, (QName('div'), Attrs()), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}div'), Attrs()), (None, -1, -1)),
             (Stream.TEXT, '\n          ', (None, -1, -1)),
             (Stream.START_NS, ('', 'http://example.org/'), (None, -1, -1)),
-            (Stream.START, (QName('p'), Attrs()), (None, -1, -1)),
-            (Stream.END, QName('p'), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}p'), Attrs()), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}p'), (None, -1, -1)),
             (Stream.END_NS, '', (None, -1, -1)),
             (Stream.TEXT, '\n          ', (None, -1, -1)),
             (Stream.START_NS, ('', 'http://example.org/'), (None, -1, -1)),
-            (Stream.START, (QName('p'), Attrs()), (None, -1, -1)),
-            (Stream.END, QName('p'), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}p'), Attrs()), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}p'), (None, -1, -1)),
             (Stream.END_NS, '', (None, -1, -1)),
             (Stream.TEXT, '\n        ', (None, -1, -1)),
-            (Stream.END, QName('div'), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}div'), (None, -1, -1)),
             (Stream.END_NS, '', (None, -1, -1))
         ])
         output = stream.render(XMLSerializer)
@@ -111,27 +111,86 @@
     def test_nested_bound_namespaces(self):
         stream = Stream([
             (Stream.START_NS, ('x', 'http://example.org/'), (None, -1, -1)),
-            (Stream.START, (QName('div'), Attrs()), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}div'), Attrs()), (None, -1, -1)),
             (Stream.TEXT, '\n          ', (None, -1, -1)),
             (Stream.START_NS, ('x', 'http://example.org/'), (None, -1, -1)),
-            (Stream.START, (QName('p'), Attrs()), (None, -1, -1)),
-            (Stream.END, QName('p'), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}p'), Attrs()), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}p'), (None, -1, -1)),
             (Stream.END_NS, 'x', (None, -1, -1)),
             (Stream.TEXT, '\n          ', (None, -1, -1)),
             (Stream.START_NS, ('x', 'http://example.org/'), (None, -1, -1)),
-            (Stream.START, (QName('p'), Attrs()), (None, -1, -1)),
-            (Stream.END, QName('p'), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}p'), Attrs()), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}p'), (None, -1, -1)),
+            (Stream.END_NS, 'x', (None, -1, -1)),
+            (Stream.TEXT, '\n        ', (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}div'), (None, -1, -1)),
+            (Stream.END_NS, 'x', (None, -1, -1))
+        ])
+        output = stream.render(XMLSerializer)
+        self.assertEqual("""<x:div xmlns:x="http://example.org/">
+          <x:p/>
+          <x:p/>
+        </x:div>""", output)
+
+    def test_multiple_default_namespaces(self):
+        stream = Stream([
+            (Stream.START, (QName('div'), Attrs()), (None, -1, -1)),
+            (Stream.TEXT, '\n          ', (None, -1, -1)),
+            (Stream.START_NS, ('', 'http://example.org/'), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}p'), Attrs()), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}p'), (None, -1, -1)),
+            (Stream.END_NS, '', (None, -1, -1)),
+            (Stream.TEXT, '\n          ', (None, -1, -1)),
+            (Stream.START_NS, ('', 'http://example.org/'), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}p'), Attrs()), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}p'), (None, -1, -1)),
+            (Stream.END_NS, '', (None, -1, -1)),
+            (Stream.TEXT, '\n        ', (None, -1, -1)),
+            (Stream.END, QName('div'), (None, -1, -1)),
+        ])
+        output = stream.render(XMLSerializer)
+        self.assertEqual("""<div>
+          <p xmlns="http://example.org/"/>
+          <p xmlns="http://example.org/"/>
+        </div>""", output)
+
+    def test_multiple_bound_namespaces(self):
+        stream = Stream([
+            (Stream.START, (QName('div'), Attrs()), (None, -1, -1)),
+            (Stream.TEXT, '\n          ', (None, -1, -1)),
+            (Stream.START_NS, ('x', 'http://example.org/'), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}p'), Attrs()), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}p'), (None, -1, -1)),
+            (Stream.END_NS, 'x', (None, -1, -1)),
+            (Stream.TEXT, '\n          ', (None, -1, -1)),
+            (Stream.START_NS, ('x', 'http://example.org/'), (None, -1, -1)),
+            (Stream.START, (QName('http://example.org/}p'), Attrs()), (None, -1, -1)),
+            (Stream.END, QName('http://example.org/}p'), (None, -1, -1)),
             (Stream.END_NS, 'x', (None, -1, -1)),
             (Stream.TEXT, '\n        ', (None, -1, -1)),
             (Stream.END, QName('div'), (None, -1, -1)),
-            (Stream.END_NS, 'x', (None, -1, -1))
         ])
         output = stream.render(XMLSerializer)
-        self.assertEqual("""<div xmlns:x="http://example.org/">
-          <p/>
-          <p/>
+        self.assertEqual("""<div>
+          <x:p xmlns:x="http://example.org/"/>
+          <x:p xmlns:x="http://example.org/"/>
         </div>""", output)
 
+    def test_atom_with_xhtml(self):
+        text = """<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
+            <id>urn:uuid:c60843aa-0da8-4fa6-bbe5-98007bc6774e</id>
+            <updated>2007-01-28T11:36:02.807108-06:00</updated>
+            <title type="xhtml">
+                <div xmlns="http://www.w3.org/1999/xhtml">Example</div>
+            </title>
+            <subtitle type="xhtml">
+                <div xmlns="http://www.w3.org/1999/xhtml">Bla bla bla</div>
+            </subtitle>
+            <icon/>
+        </feed>"""
+        output = XML(text).render(XMLSerializer)
+        self.assertEqual(text, output)
+
 
 class XHTMLSerializerTestCase(unittest.TestCase):
 
@@ -194,7 +253,7 @@
           <body>
             <button>
               <svg:svg width="600px" height="400px">
-                <svg:polygon id="triangle" points="50,50 50,300 300,300" />
+                <svg:polygon id="triangle" points="50,50 50,300 300,300"></svg:polygon>
               </svg:svg>
             </button>
           </body>
@@ -203,9 +262,9 @@
         self.assertEqual(text, output)
 
     def test_xhtml_namespace_prefix(self):
-        text = """<html:div xmlns:html="http://www.w3.org/1999/xhtml">
-            <html:strong>Hello</html:strong>
-        </html:div>"""
+        text = """<div xmlns="http://www.w3.org/1999/xhtml">
+            <strong>Hello</strong>
+        </div>"""
         output = XML(text).render(XHTMLSerializer)
         self.assertEqual(text, output)
 
@@ -257,6 +316,11 @@
           <p></p>
         </div>""", output)
 
+    def test_html5_doctype(self):
+        stream = HTML('<html></html>')
+        output = stream.render(XHTMLSerializer, doctype=DocType.HTML5)
+        self.assertEqual('<!DOCTYPE html>\n<html></html>', output)
+
 
 class HTMLSerializerTestCase(unittest.TestCase):
 
@@ -312,6 +376,11 @@
             html > body { display: none; }
         </style>""", output)
 
+    def test_html5_doctype(self):
+        stream = HTML('<html></html>')
+        output = stream.render(HTMLSerializer, doctype=DocType.HTML5)
+        self.assertEqual('<!DOCTYPE html>\n<html></html>', output)
+
 
 class EmptyTagFilterTestCase(unittest.TestCase):
 
--- a/genshi/tests/path.py
+++ b/genshi/tests/path.py
@@ -451,25 +451,17 @@
         path = Path('.[@flag="0"]/*')
         self.assertEqual('', path.select(xml).render())
 
-    # FIXME: the following two don't work due to a problem in XML serialization:
-    #        attributes that would need a namespace prefix that isn't in the
-    #        prefix map would need to get an artificial prefix, but currently
-    #        don't
-    #
-    #def test_attrname_with_namespace(self):
-    #    xml = XML('<root xmlns:f="FOO"><foo f:bar="baz"/></root>')
-    #    path = Path('foo[@f:bar]')
-    #    print path
-    #    namespaces = {'f': 'FOO'}
-    #    self.assertEqual('<foo f:bar="baz" xmlns="FOO"/>',
-    #                     path.select(xml, namespaces=namespaces).render())
-    #
-    #def test_attrwildcard_with_namespace(self):
-    #    xml = XML('<root xmlns:f="FOO"><foo f:bar="baz"/></root>')
-    #    path = Path('foo[@f:*]')
-    #    namespaces = {'f': 'FOO'}
-    #    self.assertEqual('<foo f:bar="baz" xmlns="FOO"/>',
-    #                     path.select(xml, namespaces=namespaces).render())
+    def test_attrname_with_namespace(self):
+        xml = XML('<root xmlns:f="FOO"><foo f:bar="baz"/></root>')
+        path = Path('foo[@f:bar]')
+        self.assertEqual('<foo xmlns:ns1="FOO" ns1:bar="baz"/>',
+                         path.select(xml, namespaces={'f': 'FOO'}).render())
+
+    def test_attrwildcard_with_namespace(self):
+        xml = XML('<root xmlns:f="FOO"><foo f:bar="baz"/></root>')
+        path = Path('foo[@f:*]')
+        self.assertEqual('<foo xmlns:ns1="FOO" ns1:bar="baz"/>',
+                         path.select(xml, namespaces={'f': 'FOO'}).render())
 
 
 def suite():
--- a/genshi/util.py
+++ b/genshi/util.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006-2007 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -16,6 +16,8 @@
 import htmlentitydefs
 import re
 
+__docformat__ = 'restructuredtext en'
+
 
 class LRUCache(dict):
     """A dictionary-like object that stores only a certain number of items, and
@@ -137,7 +139,9 @@
 
 
 def flatten(items):
-    """Flattens a potentially nested sequence into a flat list:
+    """Flattens a potentially nested sequence into a flat list.
+    
+    :param items: the sequence to flatten
     
     >>> flatten((1, 2))
     [1, 2]
@@ -157,6 +161,21 @@
 def plaintext(text, keeplinebreaks=True):
     """Returns the text as a `unicode` string with all entities and tags
     removed.
+    
+    >>> plaintext('<b>1 &lt; 2</b>')
+    u'1 < 2'
+    
+    The `keeplinebreaks` parameter can be set to ``False`` to replace any line
+    breaks by simple spaces:
+    
+    >>> plaintext('''<b>1
+    ... &lt;
+    ... 2</b>''', keeplinebreaks=False)
+    u'1 < 2'
+    
+    :param text: the text to convert to plain text
+    :param keeplinebreaks: whether line breaks in the text should be kept intact
+    :return: the text with tags and entities removed
     """
     text = stripentities(striptags(text))
     if not keeplinebreaks:
@@ -206,7 +225,7 @@
 
 _STRIPTAGS_RE = re.compile(r'<[^>]*?>')
 def striptags(text):
-    """Return a copy of the text with all XML/HTML tags removed.
+    """Return a copy of the text with any XML/HTML tags removed.
     
     >>> striptags('<span>Foo</span> bar')
     'Foo bar'
@@ -214,5 +233,8 @@
     'Foo'
     >>> striptags('Foo<br />')
     'Foo'
+    
+    :param text: the string to remove tags from
+    :return: the text with tags removed
     """
     return _STRIPTAGS_RE.sub('', text)
--- a/setup.py
+++ b/setup.py
@@ -20,6 +20,7 @@
     from setuptools import setup
 except ImportError:
     from distutils.core import setup
+import sys
 
 
 class build_doc(Command):
@@ -34,7 +35,8 @@
 
     def run(self):
         from docutils.core import publish_cmdline
-        conf = os.path.join('doc', 'docutils.conf')
+        docutils_conf = os.path.join('doc', 'docutils.conf')
+        epydoc_conf = os.path.join('doc', 'epydoc.conf')
 
         for source in glob('doc/*.txt'):
             dest = os.path.splitext(source)[0] + '.html'
@@ -42,7 +44,23 @@
                    os.path.getmtime(dest) < os.path.getmtime(source):
                 print 'building documentation file %s' % dest
                 publish_cmdline(writer_name='html',
-                                argv=['--config=%s' % conf, source, dest])
+                                argv=['--config=%s' % docutils_conf, source,
+                                      dest])
+
+        try:
+            from epydoc import cli
+            old_argv = sys.argv[1:]
+            sys.argv[1:] = [
+                '--config=%s' % epydoc_conf,
+                '--no-private', # epydoc bug, not read from config
+                '--simple-term',
+                '--verbose'
+            ]
+            cli.cli()
+            sys.argv[1:] = old_argv
+
+        except ImportError:
+            print 'epydoc not installed, skipping API documentation.'
 
 
 class test_doc(Command):
@@ -63,13 +81,13 @@
 
 setup(
     name = 'Genshi',
-    version = '0.4',
+    version = '0.5',
     description = 'A toolkit for stream-based generation of output for the web',
     long_description = \
-"""Genshi is a Python library that provides an integrated set of components
-for parsing, generating, and processing HTML, XML or other textual content for
-output generation on the web. The major feature is a template language, which
-is heavily inspired by Kid.""",
+"""Genshi is a Python library that provides an integrated set of
+components for parsing, generating, and processing HTML, XML or
+other textual content for output generation on the web. The major
+feature is a template language, which is heavily inspired by Kid.""",
     author = 'Edgewall Software',
     author_email = 'info@edgewall.org',
     license = 'BSD',
@@ -90,7 +108,7 @@
         'Topic :: Text Processing :: Markup :: XML'
     ],
     keywords = ['python.templating.engines'],
-    packages = ['genshi', 'genshi.template'],
+    packages = ['genshi', 'genshi.filters', 'genshi.template'],
     test_suite = 'genshi.tests.suite',
 
     extras_require = {'plugin': ['setuptools>=0.6a2']},
@@ -101,5 +119,5 @@
     genshi-text = genshi.template.plugin:TextTemplateEnginePlugin[plugin]
     """,
 
-    cmdclass={'build_doc': build_doc, 'test_doc': test_doc}
+    cmdclass = {'build_doc': build_doc, 'test_doc': test_doc}
 )
Copyright (C) 2012-2017 Edgewall Software