changeset 784:67d324a62cc0 experimental-match-fastpaths

update to 0.5.x branch, up through r907 don't know how this fits in with SoC work, but I wanted to do due diligence and keep this branch working in case it someday gets considered for trunk
author aflett
date Mon, 21 Jul 2008 23:17:52 +0000
parents 8f2c7023af94
children
files COPYING ChangeLog doc/plugin.txt doc/streams.txt doc/upgrade.txt doc/xml-templates.txt genshi/__init__.py genshi/_speedups.c genshi/builder.py genshi/core.py genshi/filters/i18n.py genshi/filters/tests/i18n.py genshi/filters/tests/transform.py genshi/filters/transform.py genshi/output.py genshi/template/base.py genshi/template/eval.py genshi/template/interpolation.py genshi/template/loader.py genshi/template/markup.py genshi/template/plugin.py genshi/template/tests/eval.py genshi/template/tests/interpolation.py genshi/template/tests/loader.py genshi/template/tests/markup.py genshi/tests/builder.py genshi/tests/output.py setup.py
diffstat 28 files changed, 2352 insertions(+), 201 deletions(-) [+]
line wrap: on
line diff
--- a/COPYING
+++ b/COPYING
@@ -1,4 +1,4 @@
-Copyright (C) 2006-2007 Edgewall Software
+Copyright (C) 2006-2008 Edgewall Software
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,26 @@
+Version 0.5.1
+http://svn.edgewall.org/repos/genshi/tags/0.5.1/
+(Jul 9 2008, from branches/stable/0.5.x)
+
+ * Fix problem with nested match templates not being applied when buffering
+   on the outer `py:match` is disabled. Thanks to Erik Bray for reporting the
+   problem and providing a test case!
+ * Fix problem in `Translator` filter that would cause the translation of
+   text nodes to fail if the translation function returned an object that was
+   not directly a string, but rather something like an instance of the
+   `LazyProxy` class in Babel (ticket #145).
+ * Fix problem with match templates incorrectly being applied multiple times.
+ * Includes from templates loaded via an absolute path now include the correct
+   file in nested directories as long if no search path has been configured
+   (ticket #240).
+ * Unbuffered match templates could result in parts of the matched content
+   being included in the output if the match template didn't actually consume
+   it via one or more calls to the `select()` function (ticket #243).
+
+
 Version 0.5
 http://svn.edgewall.org/repos/genshi/tags/0.5.0/
-(?, from branches/stable/0.5.x)
+(Jun 9 2008, from branches/stable/0.5.x)
 
  * Added #include directive for text templates (ticket #115).
  * Added new markup transformation filter contributed by Alec Thomas. This
@@ -79,6 +99,12 @@
    base class (ticket #211).
  * The `Template` class and its subclasses, as well as the interpolation API,
    now take an `filepath` parameter instead of `basedir` (ticket #207).
+ * The `XHTMLSerializer` now has a `drop_xml_decl` option that defaults to
+   `True`. Setting it to `False` will cause any XML decl in the serialized
+   stream to be included in the output as it would for XML serialization.
+ * Add support for a protocol that would allow interoperability of different
+   Python packages that generate and/or consume markup, based on the special
+   `__html__()` method (ticket #202).
 
 
 Version 0.4.4
--- a/doc/plugin.txt
+++ b/doc/plugin.txt
@@ -22,12 +22,11 @@
 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.
+Some Python web frameworks 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/
 
@@ -72,8 +71,31 @@
 format when you want to produce an Atom feed or other XML content.
 
 
+Template Paths
+--------------
+
+How you specify template paths depends on whether you have a `search path`_ set
+up or not. The search path is a list of directories that Genshi should load
+templates from. Now when you request a template using a relative path such as
+``mytmpl.html`` or ``foo/mytmpl.html``, Genshi will look for that file in the
+directories on the search path.
+
+For mostly historical reasons, the Genshi template engine plugin uses a
+different approach when you **haven't** configured the template search path:
+you now load templates using *dotted notation*, for example ``mytmpl`` or
+``foo.mytmpl``.  Note how you've lost the ability to explicitly specify the
+file extension: you now have to use ``.html`` for markup templates, and
+``.txt`` for text templates.
+
+Using the search path is recommended for a number of reasons: First, it's
+the native Genshi model and is thus more robust and better supported.
+Second, a search path gives you much more flexibility for organizing your
+application templates. And as noted above, you aren't forced to use hardcoded
+filename extensions for your template files.
+
+
 Extra Implicit Objects
-======================
+----------------------
 
 The "genshi-markup" template engine plugin adds some extra functions that are
 made available to all templates implicitly, namely:
@@ -230,6 +252,8 @@
 In the version of Genshi, the default is to use the old syntax for
 backwards-compatibility, but that will change in a future release.
 
+.. _`search path`:
+
 ``genshi.search_path``
 ----------------------
 A colon-separated list of file-system path names that the template loader should
--- a/doc/streams.txt
+++ b/doc/streams.txt
@@ -8,7 +8,7 @@
 
 
 .. contents:: Contents
-   :depth: 1
+   :depth: 2
 .. sectnum::
 
 
@@ -132,10 +132,11 @@
 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.
+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()``:
 
@@ -161,8 +162,8 @@
   <p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
 
 Both methods can be passed a ``method`` parameter that determines how exactly
-the events are serialzed to text. This parameter can be either “xml” (the
-default), “xhtml”, “html”, “text”, or a custom serializer class:
+the events are serialized to text. This parameter can be either a string or a 
+custom serializer class:
 
 .. code-block:: pycon
 
@@ -170,7 +171,7 @@
   <p class="intro">Some text and <a href="http://example.org/">a link</a>.<br></p>
 
 Note how the `<br>` element isn't closed, which is the right thing to do for
-HTML.
+HTML. See  `serialization methods`_ for more details.
 
 In addition, the ``render()`` method takes an ``encoding`` parameter, which
 defaults to “UTF-8”. If set to ``None``, the result will be a unicode string.
@@ -193,6 +194,54 @@
   Some text and a link.
 
 
+.. _`serialization methods`:
+
+Serialization Methods
+---------------------
+
+Genshi supports the use of different serialization methods to use for creating
+a text representation of a markup stream.
+
+``xml``
+  The ``XMLSerializer`` is the default serialization method and results in
+  proper XML output including namespace support, the XML declaration, CDATA
+  sections, and so on. It is not generally not suitable for serving HTML or
+  XHTML web pages (unless you want to use true XHTML 1.1), for which the
+  ``xhtml`` and ``html`` serializers described below should be preferred.
+
+``xhtml``
+  The ``XHTMLSerializer`` is a specialization of the generic ``XMLSerializer``
+  that understands the pecularities of producing XML-compliant output that can
+  also be parsed without problems by the HTML parsers found in modern web
+  browsers. Thus, the output by this serializer should be usable whether sent
+  as "text/html" or "application/xhtml+html" (although there are a lot of
+  subtle issues to pay attention to when switching between the two, in
+  particular with respect to differences in the DOM and CSS).
+
+  For example, instead of rendering a script tag as ``<script/>`` (which
+  confuses the HTML parser in many browsers), it will produce
+  ``<script></script>``. Also, it will normalize any boolean attributes values
+  that are minimized in HTML, so that for example ``<hr noshade="1"/>``
+  becomes ``<hr noshade="noshade" />``.
+
+  This serializer supports the use of namespaces for compound documents, for
+  example to use inline SVG inside an XHTML document.
+
+``html``
+  The ``HTMLSerializer`` produces proper HTML markup. The main differences
+  compared to ``xhtml`` serialization are that boolean attributes are
+  minimized, empty tags are not self-closing (so it's ``<br>`` instead of
+  ``<br />``), and that the contents of ``<script>`` and ``<style>`` elements
+  are not escaped.
+
+``text``
+  The ``TextSerializer`` produces plain text from markup streams. This is
+  useful primarily for `text templates`_, but can also be used to produce
+  plain text output from markup templates or other sources.
+
+.. _`text templates`: text-templates.html
+
+
 Serialization Options
 ---------------------
 
@@ -201,8 +250,8 @@
 options are supported by the built-in serializers:
 
 ``strip_whitespace``
-  Whether the serializer should remove trailing spaces and empty lines. Defaults
-  to ``True``.
+  Whether the serializer should remove trailing spaces and empty lines.
+  Defaults to ``True``.
 
   (This option is not available for serialization to plain text.)
 
@@ -212,6 +261,38 @@
   output. If provided, this declaration will override any ``DOCTYPE``
   declaration in the stream.
 
+  The parameter can also be specified as a string to refer to commonly used
+  doctypes:
+  
+  +-----------------------------+-------------------------------------------+
+  | Shorthand                   | DOCTYPE                                   |
+  +=============================+===========================================+
+  | ``html`` or                 | HTML 4.01 Strict                          |
+  | ``html-strict``             |                                           |
+  +-----------------------------+-------------------------------------------+
+  | ``html-transitional``       | HTML 4.01 Transitional                    |
+  +-----------------------------+-------------------------------------------+
+  | ``html-frameset``           | HTML 4.01 Frameset                        |
+  +-----------------------------+-------------------------------------------+
+  | ``html5``                   | DOCTYPE proposed for the work-in-progress |
+  |                             | HTML5 standard                            |
+  +-----------------------------+-------------------------------------------+
+  | ``xhtml`` or                | XHTML 1.0 Strict                          |
+  | ``xhtml-strict``            |                                           |
+  +-----------------------------+-------------------------------------------+
+  | ``xhtml-transitional``      | XHTML 1.0 Transitional                    |
+  +-----------------------------+-------------------------------------------+
+  | ``xhtml-frameset``          | XHTML 1.0 Frameset                        |
+  +-----------------------------+-------------------------------------------+
+  | ``xhtml11``                 | XHTML 1.1                                 |
+  +-----------------------------+-------------------------------------------+
+  | ``svg`` or ``svg-full``     | SVG 1.1                                   |
+  +-----------------------------+-------------------------------------------+
+  | ``svg-basic``               | SVG 1.1 Basic                             |
+  +-----------------------------+-------------------------------------------+
+  | ``svg-tiny``                | SVG 1.1 Tiny                              |
+  +-----------------------------+-------------------------------------------+
+
   (This option is not available for serialization to plain text.)
 
 ``namespace_prefixes``
@@ -220,6 +301,19 @@
 
   (This option is not available for serialization to HTML or plain text.)
 
+``drop_xml_decl``
+  Whether to remove the XML declaration (the ``<?xml ?>`` part at the
+  beginning of a document) when serializing. This defaults to ``True`` as an
+  XML declaration throws some older browsers into "Quirks" rendering mode.
+
+  (This option is only available for serialization to XHTML.)
+
+``strip_markup``
+  Whether the text serializer should detect and remove any tags or entity
+  encoded characters in the text.
+
+  (This option is only available for serialization to plain text.)
+
 
 
 Using XPath
@@ -285,7 +379,7 @@
 
 .. code-block:: python
 
-  START, (QName(u'p'), Attrs([(u'class', u'intro')])), pos
+  START, (QName(u'p'), Attrs([(QName(u'class'), u'intro')])), pos
 
 END
 ---
--- a/doc/upgrade.txt
+++ b/doc/upgrade.txt
@@ -56,7 +56,7 @@
 ``Markup`` Constructor
 ----------------------
 
-The ``Markup`` class now longer has a specialized constructor. The old
+The ``Markup`` class no longer has a specialized constructor. The old
 (undocumented) constructor provided a shorthand for doing positional
 substitutions. If you have code like this:
 
@@ -64,7 +64,7 @@
 
   Markup('<b>%s</b>', name)
 
-You can simply replace it by the more explicit:
+You must replace it by the more explicit:
 
 .. code-block:: python
 
--- a/doc/xml-templates.txt
+++ b/doc/xml-templates.txt
@@ -101,7 +101,8 @@
 ``py:if``
 ---------
 
-The element is only rendered if the expression evaluates to a truth value:
+The element and its content is only rendered if the expression evaluates to a
+truth value:
 
 .. code-block:: genshi
 
@@ -118,6 +119,13 @@
     <b>Hello</b>
   </div>
 
+But setting ``foo=False`` would result in the following output:
+
+.. code-block:: xml
+
+  <div>
+  </div>
+
 This directive can also be used as an element:
 
 .. code-block:: genshi
@@ -178,6 +186,15 @@
     <span>1</span>
   </div>
 
+These directives can also be used as elements:
+
+.. code-block:: genshi
+
+  <py:choose test="1">
+    <py:when test="0">0</py:when>
+    <py:when test="1">1</py:when>
+    <py:otherwise>2</py:otherwise>
+  </py:choose>
 
 Looping
 =======
--- a/genshi/__init__.py
+++ b/genshi/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006-2007 Edgewall Software
+# Copyright (C) 2006-2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -21,9 +21,13 @@
 
 __docformat__ = 'restructuredtext en'
 try:
-    __version__ = __import__('pkg_resources').get_distribution('Genshi').version
+    from pkg_resources import get_distribution, ResolutionError
+    try:
+        __version__ = get_distribution('Genshi').version
+    except ResolutionError:
+        __version__ = None # unknown
 except ImportError:
-    pass
+    __version__ = None # unknown
 
 from genshi.core import *
 from genshi.input import ParseError, XML, HTML
--- a/genshi/_speedups.c
+++ b/genshi/_speedups.c
@@ -61,6 +61,18 @@
         Py_INCREF(text);
         return text;
     }
+    if (PyObject_HasAttrString(text, "__html__")) {
+        ret = PyObject_CallMethod(text, "__html__", NULL);
+        args = PyTuple_New(1);
+        if (args == NULL) {
+            Py_DECREF(ret);
+            return NULL;
+        }
+        PyTuple_SET_ITEM(args, 0, ret);
+        ret = MarkupType.tp_new(&MarkupType, args, NULL);
+        Py_DECREF(args);
+        return ret;
+    }
     in = (PyUnicodeObject *) PyObject_Unicode(text);
     if (in == NULL) {
         return NULL;
@@ -191,6 +203,13 @@
     return escape(text, quotes);
 }
 
+static PyObject *
+Markup_html(PyObject *self)
+{
+    Py_INCREF(self);
+    return self;
+}
+
 PyDoc_STRVAR(join__doc__,
 "Return a `Markup` object which is the concatenation of the strings\n\
 in the given sequence, where this `Markup` object is the separator\n\
@@ -520,6 +539,7 @@
 } MarkupObject;
 
 static PyMethodDef Markup_methods[] = {
+    {"__html__", (PyCFunction) Markup_html, METH_NOARGS, NULL},
     {"escape", (PyCFunction) Markup_escape,
      METH_VARARGS|METH_CLASS|METH_KEYWORDS, escape__doc__},
     {"join", (PyCFunction)Markup_join, METH_VARARGS|METH_KEYWORDS, join__doc__},
--- a/genshi/builder.py
+++ b/genshi/builder.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006-2007 Edgewall Software
+# Copyright (C) 2006-2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -68,7 +68,13 @@
 Hello, <em>world</em>!
 """
 
-from genshi.core import Attrs, Namespace, QName, Stream, START, END, TEXT
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+from genshi.core import Attrs, Markup, Namespace, QName, Stream, \
+                        START, END, TEXT
 
 __all__ = ['Fragment', 'Element', 'ElementFactory', 'tag']
 __docformat__ = 'restructuredtext en'
@@ -107,6 +113,9 @@
     def __unicode__(self):
         return unicode(self.generate())
 
+    def __html__(self):
+        return Markup(self.generate())
+
     def append(self, node):
         """Append an element or string as child node.
         
@@ -146,14 +155,15 @@
         return Stream(self._generate())
 
 
-def _value_to_unicode(value):
-    if isinstance(value, unicode):
-        return value
-    return unicode(value)
-
 def _kwargs_to_attrs(kwargs):
-    return [(QName(k.rstrip('_').replace('_', '-')), _value_to_unicode(v))
-            for k, v in kwargs.items() if v is not None]
+    attrs = []
+    names = set()
+    for name, value in kwargs.items():
+        name = name.rstrip('_').replace('_', '-')
+        if value is not None and name not in names:
+            attrs.append((QName(name), unicode(value)))
+            names.add(name)
+    return Attrs(attrs)
 
 
 class Element(Fragment):
@@ -240,7 +250,7 @@
     def __init__(self, tag_, **attrib):
         Fragment.__init__(self)
         self.tag = QName(tag_)
-        self.attrib = Attrs(_kwargs_to_attrs(attrib))
+        self.attrib = _kwargs_to_attrs(attrib)
 
     def __call__(self, *args, **kwargs):
         """Append any positional arguments as child nodes, and keyword arguments
@@ -250,7 +260,7 @@
         :rtype: `Element`
         :see: `Fragment.append`
         """
-        self.attrib |= Attrs(_kwargs_to_attrs(kwargs))
+        self.attrib |= _kwargs_to_attrs(kwargs)
         Fragment.__call__(self, *args)
         return self
 
--- a/genshi/core.py
+++ b/genshi/core.py
@@ -245,6 +245,9 @@
     def __unicode__(self):
         return self.render(encoding=None)
 
+    def __html__(self):
+        return self
+
 
 START = Stream.START
 END = Stream.END
@@ -485,6 +488,9 @@
             return cls()
         if type(text) is cls:
             return text
+        if hasattr(text, '__html__'):
+            return Markup(text.__html__())
+
         text = unicode(text).replace('&', '&amp;') \
                             .replace('<', '&lt;') \
                             .replace('>', '&gt;')
--- a/genshi/filters/i18n.py
+++ b/genshi/filters/i18n.py
@@ -180,7 +180,10 @@
                     msgbuf.append(kind, data, pos)
                     continue
                 elif i18n_msg in attrs:
-                    msgbuf = MessageBuffer()
+                    params = attrs.get(i18n_msg)
+                    if params and type(params) is list: # event tuple
+                        params = params[0][1]
+                    msgbuf = MessageBuffer(params)
                     attrs -= i18n_msg
 
                 yield kind, (tag, attrs), pos
@@ -189,11 +192,14 @@
                 if not msgbuf:
                     text = data.strip()
                     if text:
-                        data = data.replace(text, translate(text))
+                        data = data.replace(text, unicode(translate(text)))
                     yield kind, data, pos
                 else:
                     msgbuf.append(kind, data, pos)
 
+            elif msgbuf and kind is EXPR:
+                msgbuf.append(kind, data, pos)
+
             elif not skip and msgbuf and kind is END:
                 msgbuf.append(kind, data, pos)
                 if not msgbuf.depth:
@@ -301,7 +307,10 @@
                 if msgbuf:
                     msgbuf.append(kind, data, pos)
                 elif i18n_msg in attrs:
-                    msgbuf = MessageBuffer(pos[1])
+                    params = attrs.get(i18n_msg)
+                    if params and type(params) is list: # event tuple
+                        params = params[0][1]
+                    msgbuf = MessageBuffer(params, pos[1])
 
             elif not skip and search_text and kind is TEXT:
                 if not msgbuf:
@@ -318,6 +327,8 @@
                     msgbuf = None
 
             elif kind is EXPR or kind is EXEC:
+                if msgbuf:
+                    msgbuf.append(kind, data, pos)
                 for funcname, strings in extract_from_code(data,
                                                            gettext_functions):
                     yield pos[1], funcname, strings
@@ -332,26 +343,46 @@
 
 
 class MessageBuffer(object):
-    """Helper class for managing localizable mixed content.
+    """Helper class for managing internationalized mixed content.
     
     :since: version 0.5
     """
 
-    def __init__(self, lineno=-1):
+    def __init__(self, params=u'', lineno=-1):
+        """Initialize the message buffer.
+        
+        :param params: comma-separated list of parameter names
+        :type params: `basestring`
+        :param lineno: the line number on which the first stream event
+                       belonging to the message was found
+        """
+        self.params = [name.strip() for name in params.split(',')]
         self.lineno = lineno
-        self.strings = []
+        self.string = []
         self.events = {}
+        self.values = {}
         self.depth = 1
         self.order = 1
         self.stack = [0]
 
     def append(self, kind, data, pos):
+        """Append a stream event to the buffer.
+        
+        :param kind: the stream event kind
+        :param data: the event data
+        :param pos: the position of the event in the source
+        """
         if kind is TEXT:
-            self.strings.append(data)
+            self.string.append(data)
             self.events.setdefault(self.stack[-1], []).append(None)
+        elif kind is EXPR:
+            param = self.params.pop(0)
+            self.string.append('%%(%s)s' % param)
+            self.events.setdefault(self.stack[-1], []).append(None)
+            self.values[param] = (kind, data, pos)
         else:
             if kind is START:
-                self.strings.append(u'[%d:' % self.order)
+                self.string.append(u'[%d:' % self.order)
                 self.events.setdefault(self.order, []).append((kind, data, pos))
                 self.stack.append(self.order)
                 self.depth += 1
@@ -360,26 +391,83 @@
                 self.depth -= 1
                 if self.depth:
                     self.events[self.stack[-1]].append((kind, data, pos))
-                    self.strings.append(u']')
+                    self.string.append(u']')
                     self.stack.pop()
 
     def format(self):
-        return u''.join(self.strings).strip()
+        """Return a message identifier representing the content in the
+        buffer.
+        """
+        return u''.join(self.string).strip()
 
-    def translate(self, string):
+    def translate(self, string, regex=re.compile(r'%\((\w+)\)s')):
+        """Interpolate the given message translation with the events in the
+        buffer and return the translated stream.
+        
+        :param string: the translated message string
+        """
         parts = parse_msg(string)
         for order, string in parts:
             events = self.events[order]
             while events:
-                event = self.events[order].pop(0)
-                if not event:
+                event = events.pop(0)
+                if event:
+                    yield event
+                else:
                     if not string:
                         break
-                    yield TEXT, string, (None, -1, -1)
+                    for idx, part in enumerate(regex.split(string)):
+                        if idx % 2:
+                            yield self.values[part]
+                        elif part:
+                            yield TEXT, part, (None, -1, -1)
                     if not self.events[order] or not self.events[order][0]:
                         break
-                else:
-                    yield event
+
+
+def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|\]')):
+    """Parse a translated message using Genshi mixed content message
+    formatting.
+
+    >>> parse_msg("See [1:Help].")
+    [(0, 'See '), (1, 'Help'), (0, '.')]
+
+    >>> parse_msg("See [1:our [2:Help] page] for details.")
+    [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')]
+
+    >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].")
+    [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')]
+
+    >>> parse_msg("[1:] Bilder pro Seite anzeigen.")
+    [(1, ''), (0, ' Bilder pro Seite anzeigen.')]
+
+    :param string: the translated message string
+    :return: a list of ``(order, string)`` tuples
+    :rtype: `list`
+    """
+    parts = []
+    stack = [0]
+    while True:
+        mo = regex.search(string)
+        if not mo:
+            break
+
+        if mo.start() or stack[-1]:
+            parts.append((stack[-1], string[:mo.start()]))
+        string = string[mo.end():]
+
+        orderno = mo.group(1)
+        if orderno is not None:
+            stack.append(int(orderno))
+        else:
+            stack.pop()
+        if not stack:
+            break
+
+    if string:
+        parts.append((stack[-1], string))
+
+    return parts
 
 
 def extract_from_code(code, gettext_functions):
@@ -425,46 +513,6 @@
                     yield funcname, strings
     return _walk(code.ast)
 
-def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|\]')):
-    """Parse a message using Genshi compound message formatting.
-
-    >>> parse_msg("See [1:Help].")
-    [(0, 'See '), (1, 'Help'), (0, '.')]
-
-    >>> parse_msg("See [1:our [2:Help] page] for details.")
-    [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')]
-
-    >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].")
-    [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')]
-    
-    >>> parse_msg("[1:] Bilder pro Seite anzeigen.")
-    [(1, ''), (0, ' Bilder pro Seite anzeigen.')]
-    
-    :since: version 0.5
-    """
-    parts = []
-    stack = [0]
-    while True:
-        mo = regex.search(string)
-        if not mo:
-            break
-
-        if mo.start() or stack[-1]:
-            parts.append((stack[-1], string[:mo.start()]))
-        string = string[mo.end():]
-
-        orderno = mo.group(1)
-        if orderno is not None:
-            stack.append(int(orderno))
-        else:
-            stack.pop()
-        if not stack:
-            break
-
-    if string:
-        parts.append((stack[-1], string))
-
-    return parts
 
 def extract(fileobj, keywords, comment_tags, options):
     """Babel extraction method for Genshi templates.
--- a/genshi/filters/tests/i18n.py
+++ b/genshi/filters/tests/i18n.py
@@ -11,6 +11,7 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://genshi.edgewall.org/log/.
 
+from datetime import datetime
 import doctest
 from StringIO import StringIO
 import unittest
@@ -262,6 +263,73 @@
           <p><input type="text" name="num"/> Eintr\xc3\xa4ge pro Seite, beginnend auf Seite <input type="text" name="num"/>.</p>
         </html>""", tmpl.generate().render())
 
+    def test_extract_i18n_msg_with_param(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="name">
+            Hello, ${user.name}!
+          </p>
+        </html>""")
+        translator = Translator()
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(1, len(messages))
+        self.assertEqual('Hello, %(name)s!', messages[0][2])
+
+    def test_translate_i18n_msg_with_param(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="name">
+            Hello, ${user.name}!
+          </p>
+        </html>""")
+        gettext = lambda s: u"Hallo, %(name)s!"
+        tmpl.filters.insert(0, Translator(gettext))
+        self.assertEqual("""<html>
+          <p>Hallo, Jim!</p>
+        </html>""", tmpl.generate(user=dict(name='Jim')).render())
+
+    def test_translate_i18n_msg_with_param_reordered(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="name">
+            Hello, ${user.name}!
+          </p>
+        </html>""")
+        gettext = lambda s: u"%(name)s, sei gegrüßt!"
+        tmpl.filters.insert(0, Translator(gettext))
+        self.assertEqual("""<html>
+          <p>Jim, sei gegrüßt!</p>
+        </html>""", tmpl.generate(user=dict(name='Jim')).render())
+
+    def test_extract_i18n_msg_with_two_params(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="name, time">
+            Posted by ${post.author} at ${entry.time.strftime('%H:%m')}
+          </p>
+        </html>""")
+        translator = Translator()
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(1, len(messages))
+        self.assertEqual('Posted by %(name)s at %(time)s', messages[0][2])
+
+    def test_translate_i18n_msg_with_two_params(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="name, time">
+            Written by ${entry.author} at ${entry.time.strftime('%H:%M')}
+          </p>
+        </html>""")
+        gettext = lambda s: u"%(name)s schrieb dies um %(time)s"
+        tmpl.filters.insert(0, Translator(gettext))
+        entry = {
+            'author': 'Jim',
+            'time': datetime(2008, 4, 1, 14, 30)
+        }
+        self.assertEqual("""<html>
+          <p>Jim schrieb dies um 14:30</p>
+        </html>""", tmpl.generate(entry=entry).render())
+
     def test_extract_i18n_msg_with_directive(self):
         tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
             xmlns:i18n="http://genshi.edgewall.org/i18n">
--- a/genshi/filters/tests/transform.py
+++ b/genshi/filters/tests/transform.py
@@ -12,20 +12,1477 @@
 # history and logs, available at http://genshi.edgewall.org/log/.
 
 import doctest
+from pprint import pprint
 import unittest
 
+from genshi import HTML
+from genshi.builder import Element
+from genshi.core import START, END, TEXT, QName, Attrs
+from genshi.filters.transform import Transformer, StreamBuffer, ENTER, EXIT, \
+                                     OUTSIDE, INSIDE, ATTR, BREAK
 import genshi.filters.transform
 
 
+FOO = '<root>ROOT<foo name="foo">FOO</foo></root>'
+FOOBAR = '<root>ROOT<foo name="foo" size="100">FOO</foo><bar name="bar">BAR</bar></root>'
+
+
+def _simplify(stream, with_attrs=False):
+    """Simplify a marked stream."""
+    def _generate():
+        for mark, (kind, data, pos) in stream:
+            if kind is START:
+                if with_attrs:
+                    data = (unicode(data[0]), dict([(unicode(k), v)
+                                                    for k, v in data[1]]))
+                else:
+                    data = unicode(data[0])
+            elif kind is END:
+                data = unicode(data)
+            elif kind is ATTR:
+                kind = ATTR
+                data = dict([(unicode(k), v) for k, v in data[1]])
+            yield mark, kind, data
+    return list(_generate())
+
+
+def _transform(html, transformer, with_attrs=False):
+    """Apply transformation returning simplified marked stream."""
+    if isinstance(html, basestring):
+        html = HTML(html)
+    stream = transformer(html, keep_marks=True)
+    return _simplify(stream, with_attrs)
+
+
+class SelectTest(unittest.TestCase):
+    """Test .select()"""
+    def _select(self, select):
+        html = HTML(FOOBAR)
+        if isinstance(select, basestring):
+            select = [select]
+        transformer = Transformer(select[0])
+        for sel in select[1:]:
+            transformer = transformer.select(sel)
+        return _transform(html, transformer)
+
+    def test_select_single_element(self):
+        self.assertEqual(
+            self._select('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')],
+            )
+
+    def test_select_context(self):
+        self.assertEqual(
+            self._select('.'),
+            [(ENTER, START, u'root'),
+             (INSIDE, TEXT, u'ROOT'),
+             (INSIDE, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, END, u'foo'),
+             (INSIDE, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (INSIDE, END, u'bar'),
+             (EXIT, END, u'root')]
+            )
+
+    def test_select_inside_select(self):
+        self.assertEqual(
+            self._select(['.', 'foo']),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')],
+            )
+
+    def test_select_text(self):
+        self.assertEqual(
+            self._select('*/text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (OUTSIDE, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, START, u'bar'),
+             (OUTSIDE, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')],
+            )
+
+    def test_select_attr(self):
+        self.assertEqual(
+            self._select('foo/@name'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ATTR, ATTR, {'name': u'foo'}),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_select_text_context(self):
+        self.assertEqual(
+            list(Transformer('.')(HTML('foo'), keep_marks=True)),
+            [('OUTSIDE', ('TEXT', u'foo', (None, 1, 0)))],
+            )
+
+
+class InvertTest(unittest.TestCase):
+    def _invert(self, select):
+        return _transform(FOO, Transformer(select).invert())
+
+    def test_invert_element(self):
+        self.assertEqual(
+            self._invert('foo'),
+            [(OUTSIDE, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (OUTSIDE, END, u'root')]
+            )
+
+    def test_invert_inverted_element(self):
+        self.assertEqual(
+            _transform(FOO, Transformer('foo').invert().invert()),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (OUTSIDE, START, u'foo'),
+             (OUTSIDE, TEXT, u'FOO'),
+             (OUTSIDE, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_invert_text(self):
+        self.assertEqual(
+            self._invert('foo/text()'),
+            [(OUTSIDE, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (OUTSIDE, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (OUTSIDE, END, u'foo'),
+             (OUTSIDE, END, u'root')]
+            )
+
+    def test_invert_attribute(self):
+        self.assertEqual(
+            self._invert('foo/@name'),
+            [(OUTSIDE, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (None, ATTR, {'name': u'foo'}),
+             (OUTSIDE, START, u'foo'),
+             (OUTSIDE, TEXT, u'FOO'),
+             (OUTSIDE, END, u'foo'),
+             (OUTSIDE, END, u'root')]
+            )
+
+    def test_invert_context(self):
+        self.assertEqual(
+            self._invert('.'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_invert_text_context(self):
+        self.assertEqual(
+            _simplify(Transformer('.').invert()(HTML('foo'), keep_marks=True)),
+            [(None, 'TEXT', u'foo')],
+            )
+
+
+
+class EndTest(unittest.TestCase):
+    def test_end(self):
+        stream = _transform(FOO, Transformer('foo').end())
+        self.assertEqual(
+            stream,
+            [(OUTSIDE, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (OUTSIDE, START, u'foo'),
+             (OUTSIDE, TEXT, u'FOO'),
+             (OUTSIDE, END, u'foo'),
+             (OUTSIDE, END, u'root')]
+            )
+
+
+class EmptyTest(unittest.TestCase):
+    def _empty(self, select):
+        return _transform(FOO, Transformer(select).empty())
+
+    def test_empty_element(self):
+        self.assertEqual(
+            self._empty('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (EXIT, END, u'foo'),
+             (None, END, u'root')],
+            )
+
+    def test_empty_text(self):
+        self.assertEqual(
+            self._empty('foo/text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (OUTSIDE, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_empty_attr(self):
+        self.assertEqual(
+            self._empty('foo/@name'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ATTR, ATTR, {'name': u'foo'}),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_empty_context(self):
+        self.assertEqual(
+            self._empty('.'),
+            [(ENTER, START, u'root'),
+             (EXIT, END, u'root')]
+            )
+
+    def test_empty_text_context(self):
+        self.assertEqual(
+            _simplify(Transformer('.')(HTML('foo'), keep_marks=True)),
+            [(OUTSIDE, TEXT, u'foo')],
+            )
+
+
+class RemoveTest(unittest.TestCase):
+    def _remove(self, select):
+        return _transform(FOO, Transformer(select).remove())
+
+    def test_remove_element(self):
+        self.assertEqual(
+            self._remove('foo|bar'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, END, u'root')]
+            )
+
+    def test_remove_text(self):
+        self.assertEqual(
+            self._remove('//text()'),
+            [(None, START, u'root'),
+             (None, START, u'foo'),
+             (None, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_remove_attr(self):
+        self.assertEqual(
+            self._remove('foo/@name'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_remove_context(self):
+        self.assertEqual(
+            self._remove('.'),
+            [],
+            )
+
+    def test_remove_text_context(self):
+        self.assertEqual(
+            _transform('foo', Transformer('.').remove()),
+            [],
+            )
+
+
+class UnwrapText(unittest.TestCase):
+    def _unwrap(self, select):
+        return _transform(FOO, Transformer(select).unwrap())
+
+    def test_unwrap_element(self):
+        self.assertEqual(
+            self._unwrap('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (INSIDE, TEXT, u'FOO'),
+             (None, END, u'root')]
+            )
+
+    def test_unwrap_text(self):
+        self.assertEqual(
+            self._unwrap('foo/text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (OUTSIDE, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_unwrap_attr(self):
+        self.assertEqual(
+            self._unwrap('foo/@name'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ATTR, ATTR, {'name': u'foo'}),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_unwrap_adjacent(self):
+        self.assertEqual(
+            _transform(FOOBAR, Transformer('foo|bar').unwrap()),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, TEXT, u'BAR'),
+             (None, END, u'root')]
+            )
+
+    def test_unwrap_root(self):
+        self.assertEqual(
+            self._unwrap('.'),
+            [(INSIDE, TEXT, u'ROOT'),
+             (INSIDE, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, END, u'foo')]
+            )
+
+    def test_unwrap_text_root(self):
+        self.assertEqual(
+            _transform('foo', Transformer('.').unwrap()),
+            [(OUTSIDE, TEXT, 'foo')],
+            )
+
+
+class WrapTest(unittest.TestCase):
+    def _wrap(self, select, wrap='wrap'):
+        return _transform(FOO, Transformer(select).wrap(wrap))
+
+    def test_wrap_element(self):
+        self.assertEqual(
+            self._wrap('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'wrap'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, END, u'wrap'),
+             (None, END, u'root')]
+            )
+
+    def test_wrap_adjacent_elements(self):
+        self.assertEqual(
+            _transform(FOOBAR, Transformer('foo|bar').wrap('wrap')),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'wrap'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, END, u'wrap'),
+             (None, START, u'wrap'),
+             (ENTER, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'bar'),
+             (None, END, u'wrap'),
+             (None, END, u'root')]
+            )
+
+    def test_wrap_text(self):
+        self.assertEqual(
+            self._wrap('foo/text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (None, START, u'wrap'),
+             (OUTSIDE, TEXT, u'FOO'),
+             (None, END, u'wrap'),
+             (None, END, u'foo'),
+             (None, END, u'root')]
+            )
+
+    def test_wrap_root(self):
+        self.assertEqual(
+            self._wrap('.'),
+            [(None, START, u'wrap'),
+             (ENTER, START, u'root'),
+             (INSIDE, TEXT, u'ROOT'),
+             (INSIDE, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, END, u'foo'),
+             (EXIT, END, u'root'),
+             (None, END, u'wrap')]
+            )
+
+    def test_wrap_text_root(self):
+        self.assertEqual(
+            _transform('foo', Transformer('.').wrap('wrap')),
+            [(None, START, u'wrap'),
+             (OUTSIDE, TEXT, u'foo'),
+             (None, END, u'wrap')],
+            )
+
+    def test_wrap_with_element(self):
+        element = Element('a', href='http://localhost')
+        self.assertEqual(
+            _transform('foo', Transformer('.').wrap(element), with_attrs=True),
+            [(None, START, (u'a', {u'href': u'http://localhost'})),
+             (OUTSIDE, TEXT, u'foo'),
+             (None, END, u'a')]
+            )
+
+
+class FilterTest(unittest.TestCase):
+    def _filter(self, select, html=FOOBAR):
+        """Returns a list of lists of filtered elements."""
+        output = []
+        def filtered(stream):
+            interval = []
+            output.append(interval)
+            for event in stream:
+                interval.append(event)
+                yield event
+        _transform(html, Transformer(select).filter(filtered))
+        simplified = []
+        for sub in output:
+            simplified.append(_simplify([(None, event) for event in sub]))
+        return simplified
+
+    def test_filter_element(self):
+        self.assertEqual(
+            self._filter('foo'),
+            [[(None, START, u'foo'),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo')]]
+            )
+
+    def test_filter_adjacent_elements(self):
+        self.assertEqual(
+            self._filter('foo|bar'),
+            [[(None, START, u'foo'),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo')],
+             [(None, START, u'bar'),
+              (None, TEXT, u'BAR'),
+              (None, END, u'bar')]]
+            )
+
+    def test_filter_text(self):
+        self.assertEqual(
+            self._filter('*/text()'),
+            [[(None, TEXT, u'FOO')],
+             [(None, TEXT, u'BAR')]]
+            )
+    def test_filter_root(self):
+        self.assertEqual(
+            self._filter('.'),
+            [[(None, START, u'root'),
+              (None, TEXT, u'ROOT'),
+              (None, START, u'foo'),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo'),
+              (None, START, u'bar'),
+              (None, TEXT, u'BAR'),
+              (None, END, u'bar'),
+              (None, END, u'root')]]
+            )
+
+    def test_filter_text_root(self):
+        self.assertEqual(
+            self._filter('.', 'foo'),
+            [[(None, TEXT, u'foo')]])
+
+
+class MapTest(unittest.TestCase):
+    def _map(self, select, kind=None):
+        data = []
+        def record(d):
+            data.append(d)
+            return d
+        _transform(FOOBAR, Transformer(select).map(record, kind))
+        return data
+
+    def test_map_element(self):
+        self.assertEqual(
+            self._map('foo'),
+            [(QName(u'foo'), Attrs([(QName(u'name'), u'foo'),
+                                    (QName(u'size'), u'100')])),
+             u'FOO',
+             QName(u'foo')]
+            )
+
+    def test_map_with_text_kind(self):
+        self.assertEqual(
+            self._map('.', TEXT),
+            [u'ROOT', u'FOO', u'BAR']
+            )
+
+    def test_map_with_root_and_end_kind(self):
+        self.assertEqual(
+            self._map('.', END),
+            [QName(u'foo'), QName(u'bar'), QName(u'root')]
+            )
+
+    def test_map_with_attribute(self):
+        self.assertEqual(
+            self._map('foo/@name'),
+            [(QName(u'foo@*'), Attrs([('name', u'foo')]))]
+            )
+
+
+class SubstituteTest(unittest.TestCase):
+    def _substitute(self, select, pattern, replace):
+        return _transform(FOOBAR, Transformer(select).substitute(pattern, replace))
+
+    def test_substitute_foo(self):
+        self.assertEqual(
+            self._substitute('foo', 'FOO|BAR', 'FOOOOO'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOOOOO'),
+             (EXIT, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_substitute_foobar_with_group(self):
+        self.assertEqual(
+            self._substitute('foo|bar', '(FOO|BAR)', r'(\1)'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'(FOO)'),
+             (EXIT, END, u'foo'),
+             (ENTER, START, u'bar'),
+             (INSIDE, TEXT, u'(BAR)'),
+             (EXIT, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+
+class RenameTest(unittest.TestCase):
+    def _rename(self, select):
+        return _transform(FOOBAR, Transformer(select).rename('foobar'))
+
+    def test_rename_root(self):
+        self.assertEqual(
+            self._rename('.'),
+            [(ENTER, START, u'foobar'),
+             (INSIDE, TEXT, u'ROOT'),
+             (INSIDE, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, END, u'foo'),
+             (INSIDE, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (INSIDE, END, u'bar'),
+             (EXIT, END, u'foobar')]
+            )
+
+    def test_rename_element(self):
+        self.assertEqual(
+            self._rename('foo|bar'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foobar'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foobar'),
+             (ENTER, START, u'foobar'),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'foobar'),
+             (None, END, u'root')]
+            )
+
+    def test_rename_text(self):
+        self.assertEqual(
+            self._rename('foo/text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (OUTSIDE, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+
+class ContentTestMixin(object):
+    def _apply(self, select, content=None, html=FOOBAR):
+        class Injector(object):
+            count = 0
+
+            def __iter__(self):
+                self.count += 1
+                return iter(HTML('CONTENT %i' % self.count))
+
+        if isinstance(html, basestring):
+            html = HTML(html)
+        if content is None:
+            content = Injector()
+        elif isinstance(content, basestring):
+            content = HTML(content)
+        return _transform(html, getattr(Transformer(select), self.operation)
+                                (content))
+
+
+class ReplaceTest(unittest.TestCase, ContentTestMixin):
+    operation = 'replace'
+
+    def test_replace_element(self):
+        self.assertEqual(
+            self._apply('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, TEXT, u'CONTENT 1'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_replace_text(self):
+        self.assertEqual(
+            self._apply('text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'CONTENT 1'),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_replace_context(self):
+        self.assertEqual(
+            self._apply('.'),
+            [(None, TEXT, u'CONTENT 1')],
+            )
+
+    def test_replace_text_context(self):
+        self.assertEqual(
+            self._apply('.', html='foo'),
+            [(None, TEXT, u'CONTENT 1')],
+            )
+
+    def test_replace_adjacent_elements(self):
+        self.assertEqual(
+            self._apply('*'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, TEXT, u'CONTENT 1'),
+             (None, TEXT, u'CONTENT 2'),
+             (None, END, u'root')],
+            )
+
+    def test_replace_all(self):
+        self.assertEqual(
+            self._apply('*|text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'CONTENT 1'),
+             (None, TEXT, u'CONTENT 2'),
+             (None, TEXT, u'CONTENT 3'),
+             (None, END, u'root')],
+            )
+
+    def test_replace_with_callback(self):
+        count = [0]
+        def content():
+            count[0] += 1
+            yield '%2i.' % count[0]
+        self.assertEqual(
+            self._apply('*', content),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, TEXT, u' 1.'),
+             (None, TEXT, u' 2.'),
+             (None, END, u'root')]
+            )
+
+
+class BeforeTest(unittest.TestCase, ContentTestMixin):
+    operation = 'before'
+
+    def test_before_element(self):
+        self.assertEqual(
+            self._apply('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, TEXT, u'CONTENT 1'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_before_text(self):
+        self.assertEqual(
+            self._apply('text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'CONTENT 1'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_before_context(self):
+        self.assertEqual(
+            self._apply('.'),
+            [(None, TEXT, u'CONTENT 1'),
+             (ENTER, START, u'root'),
+             (INSIDE, TEXT, u'ROOT'),
+             (INSIDE, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, END, u'foo'),
+             (INSIDE, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (INSIDE, END, u'bar'),
+             (EXIT, END, u'root')]
+            )
+
+    def test_before_text_context(self):
+        self.assertEqual(
+            self._apply('.', html='foo'),
+            [(None, TEXT, u'CONTENT 1'),
+             (OUTSIDE, TEXT, u'foo')]
+            )
+
+    def test_before_adjacent_elements(self):
+        self.assertEqual(
+            self._apply('*'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (None, TEXT, u'CONTENT 1'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, TEXT, u'CONTENT 2'),
+             (ENTER, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'bar'),
+             (None, END, u'root')]
+
+            )
+
+    def test_before_all(self):
+        self.assertEqual(
+            self._apply('*|text()'),
+            [(None, START, u'root'),
+             (None, TEXT, u'CONTENT 1'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (None, TEXT, u'CONTENT 2'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, TEXT, u'CONTENT 3'),
+             (ENTER, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_before_with_callback(self):
+        count = [0]
+        def content():
+            count[0] += 1
+            yield '%2i.' % count[0]
+        self.assertEqual(
+            self._apply('foo/text()', content),
+            [(None, 'START', u'root'),
+             (None, 'TEXT', u'ROOT'),
+             (None, 'START', u'foo'),
+             (None, 'TEXT', u' 1.'),
+             ('OUTSIDE', 'TEXT', u'FOO'),
+             (None, 'END', u'foo'),
+             (None, 'START', u'bar'),
+             (None, 'TEXT', u'BAR'),
+             (None, 'END', u'bar'),
+             (None, 'END', u'root')]
+            )
+
+
+class AfterTest(unittest.TestCase, ContentTestMixin):
+    operation = 'after'
+
+    def test_after_element(self):
+        self.assertEqual(
+            self._apply('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, TEXT, u'CONTENT 1'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_after_text(self):
+        self.assertEqual(
+            self._apply('text()'),
+            [(None, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (None, TEXT, u'CONTENT 1'),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_after_context(self):
+        self.assertEqual(
+            self._apply('.'),
+            [(ENTER, START, u'root'),
+             (INSIDE, TEXT, u'ROOT'),
+             (INSIDE, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, END, u'foo'),
+             (INSIDE, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (INSIDE, END, u'bar'),
+             (EXIT, END, u'root'),
+             (None, TEXT, u'CONTENT 1')]
+            )
+
+    def test_after_text_context(self):
+        self.assertEqual(
+            self._apply('.', html='foo'),
+            [(OUTSIDE, TEXT, u'foo'),
+             (None, TEXT, u'CONTENT 1')]
+            )
+
+    def test_after_adjacent_elements(self):
+        self.assertEqual(
+            self._apply('*'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, TEXT, u'CONTENT 1'),
+             (ENTER, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'bar'),
+             (None, TEXT, u'CONTENT 2'),
+             (None, END, u'root')]
+
+            )
+
+    def test_after_all(self):
+        self.assertEqual(
+            self._apply('*|text()'),
+            [(None, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (None, TEXT, u'CONTENT 1'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, TEXT, u'CONTENT 2'),
+             (ENTER, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'bar'),
+             (None, TEXT, u'CONTENT 3'),
+             (None, END, u'root')]
+            )
+
+    def test_after_with_callback(self):
+        count = [0]
+        def content():
+            count[0] += 1
+            yield '%2i.' % count[0]
+        self.assertEqual(
+            self._apply('foo/text()', content),
+            [(None, 'START', u'root'),
+             (None, 'TEXT', u'ROOT'),
+             (None, 'START', u'foo'),
+             ('OUTSIDE', 'TEXT', u'FOO'),
+             (None, 'TEXT', u' 1.'),
+             (None, 'END', u'foo'),
+             (None, 'START', u'bar'),
+             (None, 'TEXT', u'BAR'),
+             (None, 'END', u'bar'),
+             (None, 'END', u'root')]
+            )
+
+
+class PrependTest(unittest.TestCase, ContentTestMixin):
+    operation = 'prepend'
+
+    def test_prepend_element(self):
+        self.assertEqual(
+            self._apply('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (None, TEXT, u'CONTENT 1'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_prepend_text(self):
+        self.assertEqual(
+            self._apply('text()'),
+            [(None, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_prepend_context(self):
+        self.assertEqual(
+            self._apply('.'),
+            [(ENTER, START, u'root'),
+             (None, TEXT, u'CONTENT 1'),
+             (INSIDE, TEXT, u'ROOT'),
+             (INSIDE, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, END, u'foo'),
+             (INSIDE, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (INSIDE, END, u'bar'),
+             (EXIT, END, u'root')],
+            )
+
+    def test_prepend_text_context(self):
+        self.assertEqual(
+            self._apply('.', html='foo'),
+            [(OUTSIDE, TEXT, u'foo')]
+            )
+
+    def test_prepend_adjacent_elements(self):
+        self.assertEqual(
+            self._apply('*'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (None, TEXT, u'CONTENT 1'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (ENTER, START, u'bar'),
+             (None, TEXT, u'CONTENT 2'),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'bar'),
+             (None, END, u'root')]
+
+            )
+
+    def test_prepend_all(self):
+        self.assertEqual(
+            self._apply('*|text()'),
+            [(None, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (None, TEXT, u'CONTENT 1'),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (ENTER, START, u'bar'),
+             (None, TEXT, u'CONTENT 2'),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_prepend_with_callback(self):
+        count = [0]
+        def content():
+            count[0] += 1
+            yield '%2i.' % count[0]
+        self.assertEqual(
+            self._apply('foo', content),
+            [(None, 'START', u'root'),
+             (None, 'TEXT', u'ROOT'),
+             (ENTER, 'START', u'foo'),
+             (None, 'TEXT', u' 1.'),
+             (INSIDE, 'TEXT', u'FOO'),
+             (EXIT, 'END', u'foo'),
+             (None, 'START', u'bar'),
+             (None, 'TEXT', u'BAR'),
+             (None, 'END', u'bar'),
+             (None, 'END', u'root')]
+            )
+
+
+class AppendTest(unittest.TestCase, ContentTestMixin):
+    operation = 'append'
+
+    def test_append_element(self):
+        self.assertEqual(
+            self._apply('foo'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (None, TEXT, u'CONTENT 1'),
+             (EXIT, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_append_text(self):
+        self.assertEqual(
+            self._apply('text()'),
+            [(None, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (None, START, u'foo'),
+             (None, TEXT, u'FOO'),
+             (None, END, u'foo'),
+             (None, START, u'bar'),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_append_context(self):
+        self.assertEqual(
+            self._apply('.'),
+            [(ENTER, START, u'root'),
+             (INSIDE, TEXT, u'ROOT'),
+             (INSIDE, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (INSIDE, END, u'foo'),
+             (INSIDE, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (INSIDE, END, u'bar'),
+             (None, TEXT, u'CONTENT 1'),
+             (EXIT, END, u'root')],
+            )
+
+    def test_append_text_context(self):
+        self.assertEqual(
+            self._apply('.', html='foo'),
+            [(OUTSIDE, TEXT, u'foo')]
+            )
+
+    def test_append_adjacent_elements(self):
+        self.assertEqual(
+            self._apply('*'),
+            [(None, START, u'root'),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (None, TEXT, u'CONTENT 1'),
+             (EXIT, END, u'foo'),
+             (ENTER, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (None, TEXT, u'CONTENT 2'),
+             (EXIT, END, u'bar'),
+             (None, END, u'root')]
+
+            )
+
+    def test_append_all(self):
+        self.assertEqual(
+            self._apply('*|text()'),
+            [(None, START, u'root'),
+             (OUTSIDE, TEXT, u'ROOT'),
+             (ENTER, START, u'foo'),
+             (INSIDE, TEXT, u'FOO'),
+             (None, TEXT, u'CONTENT 1'),
+             (EXIT, END, u'foo'),
+             (ENTER, START, u'bar'),
+             (INSIDE, TEXT, u'BAR'),
+             (None, TEXT, u'CONTENT 2'),
+             (EXIT, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_append_with_callback(self):
+        count = [0]
+        def content():
+            count[0] += 1
+            yield '%2i.' % count[0]
+        self.assertEqual(
+            self._apply('foo', content),
+            [(None, 'START', u'root'),
+             (None, 'TEXT', u'ROOT'),
+             (ENTER, 'START', u'foo'),
+             (INSIDE, 'TEXT', u'FOO'),
+             (None, 'TEXT', u' 1.'),
+             (EXIT, 'END', u'foo'),
+             (None, 'START', u'bar'),
+             (None, 'TEXT', u'BAR'),
+             (None, 'END', u'bar'),
+             (None, 'END', u'root')]
+            )
+
+
+
+class AttrTest(unittest.TestCase):
+    def _attr(self, select, name, value):
+        return _transform(FOOBAR, Transformer(select).attr(name, value),
+                          with_attrs=True)
+
+    def test_set_existing_attr(self):
+        self.assertEqual(
+            self._attr('foo', 'name', 'FOO'),
+            [(None, START, (u'root', {})),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, (u'foo', {u'name': 'FOO', u'size': '100'})),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, START, (u'bar', {u'name': u'bar'})),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_set_new_attr(self):
+        self.assertEqual(
+            self._attr('foo', 'title', 'FOO'),
+            [(None, START, (u'root', {})),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, (u'foo', {u'name': u'foo', u'title': 'FOO', u'size': '100'})),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, START, (u'bar', {u'name': u'bar'})),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_attr_from_function(self):
+        def set(name, event):
+            self.assertEqual(name, 'name')
+            return event[1][1].get('name').upper()
+
+        self.assertEqual(
+            self._attr('foo|bar', 'name', set),
+            [(None, START, (u'root', {})),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, (u'foo', {u'name': 'FOO', u'size': '100'})),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (ENTER, START, (u'bar', {u'name': 'BAR'})),
+             (INSIDE, TEXT, u'BAR'),
+             (EXIT, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_remove_attr(self):
+        self.assertEqual(
+            self._attr('foo', 'name', None),
+            [(None, START, (u'root', {})),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, (u'foo', {u'size': '100'})),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, START, (u'bar', {u'name': u'bar'})),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+    def test_remove_attr_with_function(self):
+        def set(name, event):
+            return None
+
+        self.assertEqual(
+            self._attr('foo', 'name', set),
+            [(None, START, (u'root', {})),
+             (None, TEXT, u'ROOT'),
+             (ENTER, START, (u'foo', {u'size': '100'})),
+             (INSIDE, TEXT, u'FOO'),
+             (EXIT, END, u'foo'),
+             (None, START, (u'bar', {u'name': u'bar'})),
+             (None, TEXT, u'BAR'),
+             (None, END, u'bar'),
+             (None, END, u'root')]
+            )
+
+
+class BufferTestMixin(object):
+    def _apply(self, select, with_attrs=False):
+        buffer = StreamBuffer()
+        events = buffer.events
+
+        class Trace(object):
+            last = None
+            trace = []
+
+            def __call__(self, stream):
+                for event in stream:
+                    if events and hash(tuple(events)) != self.last:
+                        self.last = hash(tuple(events))
+                        self.trace.append(list(events))
+                    yield event
+
+        trace = Trace()
+        output = _transform(FOOBAR, getattr(Transformer(select), self.operation)
+                                    (buffer).apply(trace), with_attrs=with_attrs)
+        simplified = []
+        for interval in trace.trace:
+            simplified.append(_simplify([(None, e) for e in interval],
+                                         with_attrs=with_attrs))
+        return output, simplified
+
+
+class CopyTest(unittest.TestCase, BufferTestMixin):
+    operation = 'copy'
+
+    def test_copy_element(self):
+        self.assertEqual(
+            self._apply('foo')[1],
+            [[(None, START, u'foo'),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo')]]
+            )
+
+    def test_copy_adjacent_elements(self):
+        self.assertEqual(
+            self._apply('foo|bar')[1],
+            [[(None, START, u'foo'),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo')],
+             [(None, START, u'bar'),
+              (None, TEXT, u'BAR'),
+              (None, END, u'bar')]]
+            )
+
+    def test_copy_all(self):
+        self.assertEqual(
+            self._apply('*|text()')[1],
+            [[(None, TEXT, u'ROOT')],
+             [(None, START, u'foo'),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo')],
+             [(None, START, u'bar'),
+              (None, TEXT, u'BAR'),
+              (None, END, u'bar')]]
+            )
+
+    def test_copy_text(self):
+        self.assertEqual(
+            self._apply('*/text()')[1],
+            [[(None, TEXT, u'FOO')],
+             [(None, TEXT, u'BAR')]]
+            )
+
+    def test_copy_context(self):
+        self.assertEqual(
+            self._apply('.')[1],
+            [[(None, START, u'root'),
+              (None, TEXT, u'ROOT'),
+              (None, START, u'foo'),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo'),
+              (None, START, u'bar'),
+              (None, TEXT, u'BAR'),
+              (None, END, u'bar'),
+              (None, END, u'root')]]
+            )
+
+    def test_copy_attribute(self):
+        self.assertEqual(
+            self._apply('foo/@name', with_attrs=True)[1],
+            [[(None, ATTR, {'name': u'foo'})]]
+            )
+
+    def test_copy_attributes(self):
+        self.assertEqual(
+            self._apply('foo/@*', with_attrs=True)[1],
+            [[(None, ATTR, {u'name': u'foo', u'size': u'100'})]]
+            )
+
+
+class CutTest(unittest.TestCase, BufferTestMixin):
+    operation = 'cut'
+
+    def test_cut_element(self):
+        self.assertEqual(
+            self._apply('foo'),
+            ([(None, START, u'root'),
+              (None, TEXT, u'ROOT'),
+              (None, START, u'bar'),
+              (None, TEXT, u'BAR'),
+              (None, END, u'bar'),
+              (None, END, u'root')],
+             [[(None, START, u'foo'),
+               (None, TEXT, u'FOO'),
+               (None, END, u'foo')]])
+            )
+
+    def test_cut_adjacent_elements(self):
+        self.assertEqual(
+            self._apply('foo|bar'),
+            ([(None, START, u'root'), 
+              (None, TEXT, u'ROOT'),
+              (BREAK, BREAK, None),
+              (None, END, u'root')],
+             [[(None, START, u'foo'),
+               (None, TEXT, u'FOO'),
+               (None, END, u'foo')],
+              [(None, START, u'bar'),
+               (None, TEXT, u'BAR'),
+               (None, END, u'bar')]])
+            )
+
+    def test_cut_all(self):
+        self.assertEqual(
+            self._apply('*|text()'),
+            ([(None, 'START', u'root'),
+              ('BREAK', 'BREAK', None),
+              ('BREAK', 'BREAK', None),
+              (None, 'END', u'root')],
+             [[(None, 'TEXT', u'ROOT')],
+              [(None, 'START', u'foo'),
+               (None, 'TEXT', u'FOO'),
+               (None, 'END', u'foo')],
+              [(None, 'START', u'bar'),
+               (None, 'TEXT', u'BAR'),
+               (None, 'END', u'bar')]])
+            )
+
+    def test_cut_text(self):
+        self.assertEqual(
+            self._apply('*/text()'),
+            ([(None, 'START', u'root'),
+              (None, 'TEXT', u'ROOT'),
+              (None, 'START', u'foo'),
+              (None, 'END', u'foo'),
+              (None, 'START', u'bar'),
+              (None, 'END', u'bar'),
+              (None, 'END', u'root')],
+             [[(None, 'TEXT', u'FOO')],
+              [(None, 'TEXT', u'BAR')]])
+            )
+
+    def test_cut_context(self):
+        self.assertEqual(
+            self._apply('.')[1],
+            [[(None, 'START', u'root'),
+              (None, 'TEXT', u'ROOT'),
+              (None, 'START', u'foo'),
+              (None, 'TEXT', u'FOO'),
+              (None, 'END', u'foo'),
+              (None, 'START', u'bar'),
+              (None, 'TEXT', u'BAR'),
+              (None, 'END', u'bar'),
+              (None, 'END', u'root')]]
+            )
+
+    def test_cut_attribute(self):
+        self.assertEqual(
+            self._apply('foo/@name', with_attrs=True),
+            ([(None, START, (u'root', {})),
+              (None, TEXT, u'ROOT'),
+              (None, START, (u'foo', {u'size': u'100'})),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo'),
+              (None, START, (u'bar', {u'name': u'bar'})),
+              (None, TEXT, u'BAR'),
+              (None, END, u'bar'),
+              (None, END, u'root')],
+             [[(None, ATTR, {u'name': u'foo'})]])
+            )
+
+    def test_cut_attributes(self):
+        self.assertEqual(
+            self._apply('foo/@*', with_attrs=True),
+            ([(None, START, (u'root', {})),
+              (None, TEXT, u'ROOT'),
+              (None, START, (u'foo', {})),
+              (None, TEXT, u'FOO'),
+              (None, END, u'foo'),
+              (None, START, (u'bar', {u'name': u'bar'})),
+              (None, TEXT, u'BAR'),
+              (None, END, u'bar'),
+              (None, END, u'root')],
+             [[(None, ATTR, {u'name': u'foo', u'size': u'100'})]])
+            )
+
+# XXX Test this when the XPath implementation is fixed (#233).
+#    def test_cut_attribute_or_attribute(self):
+#        self.assertEqual(
+#            self._apply('foo/@name | foo/@size', with_attrs=True),
+#            ([(None, START, (u'root', {})),
+#              (None, TEXT, u'ROOT'),
+#              (None, START, (u'foo', {})),
+#              (None, TEXT, u'FOO'),
+#              (None, END, u'foo'),
+#              (None, START, (u'bar', {u'name': u'bar'})),
+#              (None, TEXT, u'BAR'),
+#              (None, END, u'bar'),
+#              (None, END, u'root')],
+#             [[(None, ATTR, {u'name': u'foo', u'size': u'100'})]])
+#            )
+
+
+
+
 def suite():
     from genshi.input import HTML
     from genshi.core import Markup
     from genshi.builder import tag
-    suite = doctest.DocTestSuite(genshi.filters.transform,
-                                 optionflags=doctest.NORMALIZE_WHITESPACE,
-                                 extraglobs={'HTML': HTML, 'tag': tag,
-                                     'Markup': Markup})
+    suite = unittest.TestSuite()
+    for test in (SelectTest, InvertTest, EndTest,
+                 EmptyTest, RemoveTest, UnwrapText, WrapTest, FilterTest,
+                 MapTest, SubstituteTest, RenameTest, ReplaceTest, BeforeTest,
+                 AfterTest, PrependTest, AppendTest, AttrTest, CopyTest, CutTest):
+        suite.addTest(unittest.makeSuite(test, 'test'))
+    suite.addTest(doctest.DocTestSuite(
+        genshi.filters.transform, optionflags=doctest.NORMALIZE_WHITESPACE,
+        extraglobs={'HTML': HTML, 'tag': tag, 'Markup': Markup}))
     return suite
 
+
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
--- a/genshi/filters/transform.py
+++ b/genshi/filters/transform.py
@@ -55,7 +55,7 @@
 from genshi.path import Path
 
 __all__ = ['Transformer', 'StreamBuffer', 'InjectorTransformation', 'ENTER',
-           'EXIT', 'INSIDE', 'OUTSIDE']
+           'EXIT', 'INSIDE', 'OUTSIDE', 'BREAK']
 
 
 class TransformMark(str):
@@ -86,6 +86,40 @@
 """Stream augmentation mark indicating that a selected element is being
 exited."""
 
+BREAK = TransformMark('BREAK')
+"""Stream augmentation mark indicating a break between two otherwise contiguous
+blocks of marked events.
+
+This is used primarily by the cut() transform to provide later transforms with
+an opportunity to operate on the cut buffer.
+"""
+
+
+class PushBackStream(object):
+    """Allows a single event to be pushed back onto the stream and re-consumed.
+    """
+    def __init__(self, stream):
+        self.stream = iter(stream)
+        self.peek = None
+
+    def push(self, event):
+        assert self.peek is None
+        self.peek = event
+
+    def __iter__(self):
+        while True:
+            if self.peek is not None:
+                peek = self.peek
+                self.peek = None
+                yield peek
+            else:
+                try:
+                    event = self.stream.next()
+                    yield event
+                except StopIteration:
+                    if self.peek is None:
+                        raise
+
 
 class Transformer(object):
     """Stream filter that can apply a variety of different transformations to
@@ -150,17 +184,21 @@
         """
         self.transforms = [SelectTransformation(path)]
 
-    def __call__(self, stream):
+    def __call__(self, stream, keep_marks=False):
         """Apply the transform filter to the marked stream.
 
         :param stream: the marked event stream to filter
+        :param keep_marks: Do not strip transformer selection marks from the
+                           stream. Useful for testing.
         :return: the transformed stream
         :rtype: `Stream`
         """
         transforms = self._mark(stream)
         for link in self.transforms:
             transforms = link(transforms)
-        return Stream(self._unmark(transforms),
+        if not keep_marks:
+            transforms = self._unmark(transforms)
+        return Stream(transforms,
                       serializer=getattr(stream, 'serializer', None))
 
     def apply(self, function):
@@ -329,7 +367,8 @@
         <html><head><title>New Title</title></head><body>Some <em>body</em>
         text.</body></html>
 
-        :param content: Either an iterable of events or a string to insert.
+        :param content: Either a callable, an iterable of events, or a string
+                        to insert.
         :rtype: `Transformer`
         """
         return self.apply(ReplaceTransformation(content))
@@ -346,7 +385,8 @@
         <html><head><title>Some Title</title></head><body>Some emphasised
         <em>body</em> text.</body></html>
 
-        :param content: Either an iterable of events or a string to insert.
+        :param content: Either a callable, an iterable of events, or a string
+                        to insert.
         :rtype: `Transformer`
         """
         return self.apply(BeforeTransformation(content))
@@ -362,7 +402,8 @@
         <html><head><title>Some Title</title></head><body>Some <em>body</em>
         rock text.</body></html>
 
-        :param content: Either an iterable of events or a string to insert.
+        :param content: Either a callable, an iterable of events, or a string
+                        to insert.
         :rtype: `Transformer`
         """
         return self.apply(AfterTransformation(content))
@@ -378,7 +419,8 @@
         <html><head><title>Some Title</title></head><body>Some new body text.
         Some <em>body</em> text.</body></html>
 
-        :param content: Either an iterable of events or a string to insert.
+        :param content: Either a callable, an iterable of events, or a string
+                        to insert.
         :rtype: `Transformer`
         """
         return self.apply(PrependTransformation(content))
@@ -392,7 +434,8 @@
         <html><head><title>Some Title</title></head><body>Some <em>body</em>
         text. Some new body text.</body></html>
 
-        :param content: Either an iterable of events or a string to insert.
+        :param content: Either a callable, an iterable of events, or a string
+                        to insert.
         :rtype: `Transformer`
         """
         return self.apply(AppendTransformation(content))
@@ -440,9 +483,13 @@
 
     #{ Buffer operations
 
-    def copy(self, buffer):
+    def copy(self, buffer, accumulate=False):
         """Copy selection into buffer.
 
+        The buffer is replaced by each *contiguous* selection before being passed
+        to the next transformation. If accumulate=True, further selections will
+        be appended to the buffer rather than replacing it.
+
         >>> from genshi.builder import tag
         >>> buffer = StreamBuffer()
         >>> html = HTML('<html><head><title>Some Title</title></head>'
@@ -452,17 +499,14 @@
         <html><head><title>Some Title</title></head><body><h1>Some
         Title</h1>Some <em>body</em> text.</body></html>
 
-        To ensure that a transformation can be reused deterministically, the
-        contents of ``buffer`` is replaced by the ``copy()`` operation:
+        This example illustrates that only a single contiguous selection will
+        be buffered:
 
-        >>> print buffer
-        Some Title
         >>> print html | Transformer('head/title/text()').copy(buffer) \\
         ...     .end().select('body/em').copy(buffer).end().select('body') \\
         ...     .prepend(tag.h1(buffer))
-        <html><head><title>Some
-        Title</title></head><body><h1><em>body</em></h1>Some <em>body</em>
-        text.</body></html>
+        <html><head><title>Some Title</title></head><body><h1>Some
+        Title</h1>Some <em>body</em> text.</body></html>
         >>> print buffer
         <em>body</em>
 
@@ -475,7 +519,8 @@
         >>> def apply_attr(name, entry):
         ...     return list(buffer)[0][1][1].get('class')
         >>> print html | Transformer('body/em[@class]/@class').copy(buffer) \\
-        ...     .end().select('body/em[not(@class)]').attr('class', apply_attr)
+        ...     .end().buffer().select('body/em[not(@class)]') \\
+        ...     .attr('class', apply_attr)
         <html><head><title>Some Title</title></head><body><em
         class="before">Some</em> <em class="before">body</em><em
         class="before">text</em>.</body></html>
@@ -484,11 +529,18 @@
         :param buffer: the `StreamBuffer` in which the selection should be
                        stored
         :rtype: `Transformer`
-        :note: this transformation will buffer the entire input stream
+        :note: Copy (and cut) copy each individual selected object into the
+               buffer before passing to the next transform. For example, the
+               XPath ``*|text()`` will select all elements and text, each
+               instance of which will be copied to the buffer individually
+               before passing to the next transform. This has implications for
+               how ``StreamBuffer`` objects can be used, so some
+               experimentation may be required.
+
         """
-        return self.apply(CopyTransformation(buffer))
+        return self.apply(CopyTransformation(buffer, accumulate))
 
-    def cut(self, buffer):
+    def cut(self, buffer, accumulate=False):
         """Copy selection into buffer and remove the selection from the stream.
 
         >>> from genshi.builder import tag
@@ -500,12 +552,40 @@
         <html><head><title>Some Title</title></head><body>Some
         <em/><h1>body</h1> text.</body></html>
 
+        Specifying accumulate=True, appends all selected intervals onto the
+        buffer. Combining this with the .buffer() operation allows us operate
+        on all copied events rather than per-segment. See the documentation on
+        buffer() for more information.
+
         :param buffer: the `StreamBuffer` in which the selection should be
                        stored
         :rtype: `Transformer`
         :note: this transformation will buffer the entire input stream
         """
-        return self.apply(CutTransformation(buffer))
+        return self.apply(CutTransformation(buffer, accumulate))
+
+    def buffer(self):
+        """Buffer the entire stream (can consume a considerable amount of
+        memory).
+
+        Useful in conjunction with copy(accumulate=True) and
+        cut(accumulate=True) to ensure that all marked events in the entire
+        stream are copied to the buffer before further transformations are
+        applied.
+
+        For example, to move all <note> elements inside a <notes> tag at the
+        top of the document:
+
+        >>> doc = HTML('<doc><notes></notes><body>Some <note>one</note> '
+        ...            'text <note>two</note>.</body></doc>')
+        >>> buffer = StreamBuffer()
+        >>> print doc | Transformer('body/note').cut(buffer, accumulate=True) \\
+        ...     .end().buffer().select('notes').prepend(buffer)
+        <doc><notes><note>one</note><note>two</note></notes><body>Some  text
+        .</body></doc>
+
+        """
+        return self.apply(list)
 
     #{ Miscellaneous operations
 
@@ -546,13 +626,17 @@
         Refer to the documentation for ``re.sub()`` for details.
 
         >>> html = HTML('<html><body>Some text, some more text and '
-        ...             '<b>some bold text</b></body></html>')
-        >>> print html | Transformer('body').substitute('(?i)some', 'SOME')
-        <html><body>SOME text, some more text and <b>SOME bold text</b></body></html>
-        >>> tags = tag.html(tag.body('Some text, some more text and ',
+        ...             '<b>some bold text</b>\\n'
+        ...             '<i>some italicised text</i></body></html>')
+        >>> print html | Transformer('body/b').substitute('(?i)some', 'SOME')
+        <html><body>Some text, some more text and <b>SOME bold text</b>
+        <i>some italicised text</i></body></html>
+        >>> tags = tag.html(tag.body('Some text, some more text and\\n',
         ...      Markup('<b>some bold text</b>')))
-        >>> print tags.generate() | Transformer('body').substitute('(?i)some', 'SOME')
-        <html><body>SOME text, some more text and <b>SOME bold text</b></body></html>
+        >>> print tags.generate() | Transformer('body').substitute(
+        ...     '(?i)some', 'SOME')
+        <html><body>SOME text, some more text and
+        <b>SOME bold text</b></body></html>
 
         :param pattern: A regular expression object or string.
         :param replace: Replacement pattern.
@@ -600,7 +684,8 @@
 
     def _unmark(self, stream):
         for mark, event in stream:
-            if event[0] is not None:
+            kind = event[0]
+            if not (kind is None or kind is ATTR or kind is BREAK):
                 yield event
 
 
@@ -652,9 +737,12 @@
             elif isinstance(result, Attrs):
                 # XXX  Selected *attributes* are given a "kind" of None to
                 # indicate they are not really part of the stream.
-                yield ATTR, (None, (QName(event[1][0] + '@*'), result), event[2])
+                yield ATTR, (ATTR, (QName(event[1][0] + '@*'), result), event[2])
                 yield None, event
+            elif isinstance(result, tuple):
+                yield OUTSIDE, result
             elif result:
+                # XXX Assume everything else is "text"?
                 yield None, (TEXT, unicode(result), (None, -1, -1))
             else:
                 yield None, event
@@ -700,8 +788,12 @@
         :param stream: the marked event stream to filter
         """
         for mark, event in stream:
-            if mark not in (INSIDE, OUTSIDE):
-                yield mark, event
+            yield mark, event
+            if mark is ENTER:
+                for mark, event in stream:
+                    if mark is EXIT:
+                        yield mark, event
+                        break
 
 
 class RemoveTransformation(object):
@@ -746,16 +838,21 @@
                 for prefix in element[:-1]:
                     yield None, prefix
                 yield mark, event
-                while True:
-                    try:
-                        mark, event = stream.next()
-                    except StopIteration:
-                        yield None, element[-1]
+                start = mark
+                stopped = False
+                for mark, event in stream:
+                    if start is ENTER and mark is EXIT:
+                        yield mark, event
+                        stopped = True
+                        break
                     if not mark:
                         break
                     yield mark, event
+                else:
+                    stopped = True
                 yield None, element[-1]
-                yield mark, event
+                if not stopped:
+                    yield mark, event
             else:
                 yield mark, event
 
@@ -784,7 +881,7 @@
 
 class FilterTransformation(object):
     """Apply a normal stream filter to the selection. The filter is called once
-    for each contiguous block of marked events."""
+    for each selection."""
 
     def __init__(self, filter):
         """Create the transform.
@@ -806,14 +903,31 @@
 
         queue = []
         for mark, event in stream:
-            if mark:
+            if mark is ENTER:
                 queue.append(event)
-            else:
+                for mark, event in stream:
+                    queue.append(event)
+                    if mark is EXIT:
+                        break
                 for queue_event in flush(queue):
                     yield queue_event
-                yield None, event
-        for event in flush(queue):
-            yield event
+            elif mark is OUTSIDE:
+                stopped = True
+                queue.append(event)
+                for mark, event in stream:
+                    if mark is not OUTSIDE:
+                        break
+                    queue.append(event)
+                else:
+                    stopped = True
+                for queue_event in flush(queue):
+                    yield queue_event
+                if not stopped:
+                    yield None, event
+            else:
+                yield mark, event
+        for queue_event in flush(queue):
+            yield queue_event
 
 
 class MapTransformation(object):
@@ -848,7 +962,7 @@
 
     Refer to the documentation for ``re.sub()`` for details.
     """
-    def __init__(self, pattern, replace, count=1):
+    def __init__(self, pattern, replace, count=0):
         """Create the transform.
 
         :param pattern: A regular expression object, or string.
@@ -868,7 +982,7 @@
         :param stream: The marked event stream to filter
         """
         for mark, (kind, data, pos) in stream:
-            if kind is TEXT:
+            if mark is not None and kind is TEXT:
                 new_data = self.pattern.sub(self.replace, data, self.count)
                 if isinstance(data, Markup):
                     data = Markup(new_data)
@@ -922,7 +1036,10 @@
         self.content = content
 
     def _inject(self):
-        for event in _ensure(self.content):
+        content = self.content
+        if callable(content):
+            content = content()
+        for event in _ensure(content):
             yield None, event
 
 
@@ -934,14 +1051,18 @@
 
         :param stream: The marked event stream to filter
         """
+        stream = PushBackStream(stream)
         for mark, event in stream:
             if mark is not None:
+                start = mark
                 for subevent in self._inject():
                     yield subevent
-                while True:
-                    mark, event = stream.next()
-                    if mark is None:
-                        yield mark, event
+                for mark, event in stream:
+                    if start is ENTER:
+                        if mark is EXIT:
+                            break
+                    elif mark != start:
+                        stream.push((mark, event))
                         break
             else:
                 yield mark, event
@@ -955,17 +1076,22 @@
 
         :param stream: The marked event stream to filter
         """
+        stream = PushBackStream(stream)
         for mark, event in stream:
             if mark is not None:
+                start = mark
                 for subevent in self._inject():
                     yield subevent
                 yield mark, event
-                while True:
-                    mark, event = stream.next()
-                    if not mark:
+                for mark, event in stream:
+                    if mark != start and start is not ENTER:
+                        stream.push((mark, event))
                         break
                     yield mark, event
-            yield mark, event
+                    if start is ENTER and mark is EXIT:
+                        break
+            else:
+                yield mark, event
 
 
 class AfterTransformation(InjectorTransformation):
@@ -976,20 +1102,20 @@
 
         :param stream: The marked event stream to filter
         """
+        stream = PushBackStream(stream)
         for mark, event in stream:
             yield mark, event
             if mark:
-                while True:
-                    try:
-                        mark, event = stream.next()
-                    except StopIteration:
-                        break
-                    if not mark:
+                start = mark
+                for mark, event in stream:
+                    if start is not ENTER and mark != start:
+                        stream.push((mark, event))
                         break
                     yield mark, event
+                    if start is ENTER and mark is EXIT:
+                        break
                 for subevent in self._inject():
                     yield subevent
-                yield mark, event
 
 
 class PrependTransformation(InjectorTransformation):
@@ -1002,7 +1128,7 @@
         """
         for mark, event in stream:
             yield mark, event
-            if mark in (ENTER, OUTSIDE):
+            if mark is ENTER:
                 for subevent in self._inject():
                     yield subevent
 
@@ -1018,8 +1144,7 @@
         for mark, event in stream:
             yield mark, event
             if mark is ENTER:
-                while True:
-                    mark, event = stream.next()
+                for mark, event in stream:
                     if mark is EXIT:
                         break
                     yield mark, event
@@ -1076,32 +1201,50 @@
         self.events.append(event)
 
     def reset(self):
-        """Reset the buffer so that it's empty."""
+        """Empty the buffer of events."""
         del self.events[:]
 
 
 class CopyTransformation(object):
     """Copy selected events into a buffer for later insertion."""
 
-    def __init__(self, buffer):
+    def __init__(self, buffer, accumulate=False):
         """Create the copy transformation.
 
         :param buffer: the `StreamBuffer` in which the selection should be
                        stored
         """
+        if not accumulate:
+            buffer.reset()
         self.buffer = buffer
+        self.accumulate = accumulate
 
     def __call__(self, stream):
         """Apply the transformation to the marked stream.
 
         :param stream: the marked event stream to filter
         """
-        self.buffer.reset()
-        stream = list(stream)
+        stream = PushBackStream(stream)
+
         for mark, event in stream:
             if mark:
+                if not self.accumulate:
+                    self.buffer.reset()
+                events = [(mark, event)]
                 self.buffer.append(event)
-        return stream
+                start = mark
+                for mark, event in stream:
+                    if start is not ENTER and mark != start:
+                        stream.push((mark, event))
+                        break
+                    events.append((mark, event))
+                    self.buffer.append(event)
+                    if start is ENTER and mark is EXIT:
+                        break
+                for i in events:
+                    yield i
+            else:
+                yield mark, event
 
 
 class CutTransformation(object):
@@ -1109,36 +1252,58 @@
     selection.
     """
 
-    def __init__(self, buffer):
+    def __init__(self, buffer, accumulate=False):
         """Create the cut transformation.
 
         :param buffer: the `StreamBuffer` in which the selection should be
                        stored
         """
         self.buffer = buffer
+        self.accumulate = accumulate
+
 
     def __call__(self, stream):
         """Apply the transform filter to the marked stream.
 
         :param stream: the marked event stream to filter
         """
-        out_stream = []
-        attributes = None
-        for mark, (kind, data, pos) in stream:
-            if attributes:
-                assert kind is START
-                data = (data[0], data[1] - attributes)
-                attributes = None
+        attributes = []
+        stream = PushBackStream(stream)
+        broken = False
+        if not self.accumulate:
+            self.buffer.reset()
+        for mark, event in stream:
             if mark:
-                # There is some magic here. ATTR marked events are pushed into
-                # the stream *before* the START event they originated from.
-                # This allows cut() to strip out the attributes from START
-                # event as would be expected.
+                # Send a BREAK event if there was no other event sent between 
+                if not self.accumulate:
+                    if not broken and self.buffer:
+                        yield BREAK, (BREAK, None, None)
+                    self.buffer.reset()
+                self.buffer.append(event)
+                start = mark
                 if mark is ATTR:
-                    self.buffer.append((kind, data, pos))
-                    attributes = [name for name, _ in data[1]]
-                else:
-                    self.buffer.append((kind, data, pos))
+                    attributes.extend([name for name, _ in event[1][1]])
+                for mark, event in stream:
+                    if start is mark is ATTR:
+                        attributes.extend([name for name, _ in event[1][1]])
+                    # Handle non-element contiguous selection
+                    if start is not ENTER and mark != start:
+                        # Operating on the attributes of a START event
+                        if start is ATTR:
+                            kind, data, pos = event
+                            assert kind is START
+                            data = (data[0], data[1] - attributes)
+                            attributes = None
+                            stream.push((mark, (kind, data, pos)))
+                        else:
+                            stream.push((mark, event))
+                        break
+                    self.buffer.append(event)
+                    if start is ENTER and mark is EXIT:
+                        break
+                broken = False
             else:
-                out_stream.append((mark, (kind, data, pos)))
-        return out_stream
+                broken = True
+                yield mark, event
+        if not broken and self.buffer:
+            yield BREAK, (BREAK, None, None)
--- a/genshi/output.py
+++ b/genshi/output.py
@@ -114,6 +114,11 @@
     )
     XHTML = XHTML_STRICT
 
+    XHTML11 = (
+        'html', '-//W3C//DTD XHTML 1.1//EN',
+        'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
+    )
+
     SVG_FULL = (
         'svg', '-//W3C//DTD SVG 1.1//EN',
         'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
@@ -135,11 +140,12 @@
         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
+         * "html-frameset" 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
+         * "xhtml11" for the XHTML 1.1 DTD
          * "svg" or "svg-full" for the SVG 1.1 DTD
          * "svg-basic" for the SVG Basic 1.1 DTD
          * "svg-tiny" for the SVG Tiny 1.1 DTD
@@ -157,6 +163,7 @@
             'xhtml': cls.XHTML, 'xhtml-strict': cls.XHTML_STRICT,
             'xhtml-transitional': cls.XHTML_TRANSITIONAL,
             'xhtml-frameset': cls.XHTML_FRAMESET,
+            'xhtml11': cls.XHTML11,
             'svg': cls.SVG, 'svg-full': cls.SVG_FULL,
             'svg-basic': cls.SVG_BASIC,
             'svg-tiny': cls.SVG_TINY
@@ -280,7 +287,7 @@
     ])
 
     def __init__(self, doctype=None, strip_whitespace=True,
-                 namespace_prefixes=None):
+                 namespace_prefixes=None, drop_xml_decl=True):
         super(XHTMLSerializer, self).__init__(doctype, False)
         self.filters = [EmptyTagFilter()]
         if strip_whitespace:
@@ -290,11 +297,13 @@
         self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
         if doctype:
             self.filters.append(DocTypeInserter(doctype))
+        self.drop_xml_decl = drop_xml_decl
 
     def __call__(self, stream):
         boolean_attrs = self._BOOLEAN_ATTRS
         empty_elems = self._EMPTY_ELEMS
-        have_doctype = False
+        drop_xml_decl = self.drop_xml_decl
+        have_decl = have_doctype = False
         in_cdata = False
 
         for filter_ in self.filters:
@@ -346,6 +355,18 @@
                 yield Markup(u''.join(buf)) % filter(None, data)
                 have_doctype = True
 
+            elif kind is XML_DECL and not have_decl and not drop_xml_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 START_CDATA:
                 yield Markup('<![CDATA[')
                 in_cdata = True
@@ -473,7 +494,7 @@
     >>> print elem.generate().render(TextSerializer)
     <a href="foo">Hello &amp; Bye!</a><br/>
     
-    You can use the `strip_markup` to change this behavior, so that tags and
+    You can use the ``strip_markup`` to change this behavior, so that tags and
     entities are stripped from the output (or in the case of entities,
     replaced with the equivalent character):
 
@@ -482,6 +503,11 @@
     """
 
     def __init__(self, strip_markup=False):
+        """Create the serializer.
+        
+        :param strip_markup: whether markup (tags and encoded characters) found
+                             in the text should be removed
+        """
         self.strip_markup = strip_markup
 
     def __call__(self, stream):
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -244,6 +244,10 @@
         """
         return [(key, self.get(key)) for key in self.keys()]
 
+    def update(self, mapping):
+        """Update the context from the mapping provided."""
+        self.frames[0].update(mapping)
+
     def push(self, data):
         """Push a new scope on the stack.
         
--- a/genshi/template/eval.py
+++ b/genshi/template/eval.py
@@ -22,7 +22,6 @@
 except NameError:
     from sets import ImmutableSet as frozenset
     from sets import Set as set
-import sys
 from textwrap import dedent
 
 from genshi.core import Markup
@@ -33,6 +32,29 @@
            'Undefined', 'UndefinedError']
 __docformat__ = 'restructuredtext en'
 
+# Check for a Python 2.4 bug in the eval loop
+has_star_import_bug = False
+try:
+    class _FakeMapping(object):
+        __getitem__ = __setitem__ = lambda *a: None
+    exec 'from sys import *' in {}, _FakeMapping()
+except SystemError:
+    has_star_import_bug = True
+except TypeError:
+    pass # Python 2.3
+del _FakeMapping
+
+def _star_import_patch(mapping, modname):
+    """This function is used as helper if a Python version with a broken
+    star-import opcode is in use.
+    """
+    module = __import__(modname, None, None, ['__all__'])
+    if hasattr(module, '__all__'):
+        members = module.__all__
+    else:
+        members = [x for x in module.__dict__ if not x.startswith('_')]
+    mapping.update([(name, getattr(module, name)) for name in members])
+
 
 class Code(object):
     """Abstract base class for the `Expression` and `Suite` classes."""
@@ -270,6 +292,7 @@
             '_lookup_name': cls.lookup_name,
             '_lookup_attr': cls.lookup_attr,
             '_lookup_item': cls.lookup_item,
+            '_star_import_patch': _star_import_patch,
             'UndefinedError': UndefinedError,
         }
     globals = classmethod(globals)
@@ -462,7 +485,7 @@
         if lineno is not None:
             node.lineno = lineno
         if isinstance(node, (ast.Class, ast.Function, ast.Lambda)) or \
-                sys.version_info > (2, 4) and isinstance(node, ast.GenExpr):
+                hasattr(ast, 'GenExpr') and isinstance(node, ast.GenExpr):
             node.filename = '<string>' # workaround for bug in pycodegen
         return node
 
@@ -492,6 +515,19 @@
             node.doc, self.visit(node.code)
         )
 
+    def visitFrom(self, node):
+        if not has_star_import_bug or node.names != [('*', None)]:
+            # This is a Python 2.4 bug. Only if we have a broken Python
+            # version we have to apply the hack
+            return node
+        new_node = ast.Discard(ast.CallFunc(
+            ast.Name('_star_import_patch'),
+            [ast.Name('__data__'), ast.Const(node.modname)], None, None
+        ))
+        if hasattr(node, 'lineno'): # No lineno in Python 2.3
+            new_node.lineno = node.lineno
+        return new_node
+
     def visitFunction(self, node):
         args = []
         if hasattr(node, 'decorators'):
--- a/genshi/template/interpolation.py
+++ b/genshi/template/interpolation.py
@@ -17,7 +17,8 @@
 
 from itertools import chain
 import os
-from tokenize import tokenprog
+import re
+from tokenize import PseudoToken
 
 from genshi.core import TEXT
 from genshi.template.base import TemplateSyntaxError, EXPR
@@ -30,6 +31,11 @@
 NAMECHARS = NAMESTART + '.0123456789'
 PREFIX = '$'
 
+token_re = re.compile('%s|%s(?s)' % (
+    r'[uU]?[rR]?("""|\'\'\')((?<!\\)\\\1|.)*?\1',
+    PseudoToken
+))
+
 def interpolate(text, filepath=None, lineno=-1, offset=0, lookup='strict'):
     """Parse the given string and extract expressions.
     
@@ -106,7 +112,7 @@
             pos = offset + 2
             level = 1
             while level:
-                match = tokenprog.match(text, pos)
+                match = token_re.match(text, pos)
                 if match is None:
                     raise TemplateSyntaxError('invalid syntax',  filepath,
                                               *textpos[1:])
--- a/genshi/template/loader.py
+++ b/genshi/template/loader.py
@@ -22,7 +22,8 @@
 from genshi.template.base import TemplateError
 from genshi.util import LRUCache
 
-__all__ = ['TemplateLoader', 'TemplateNotFound']
+__all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package',
+           'prefixed']
 __docformat__ = 'restructuredtext en'
 
 
@@ -163,8 +164,14 @@
         """
         if cls is None:
             cls = self.default_class
-        if relative_to and not os.path.isabs(relative_to):
+        search_path = self.search_path
+
+        # Make the filename relative to the template file its being loaded
+        # from, but only if that file is specified as a relative path, or no
+        # search path has been set up
+        if relative_to and (not search_path or not os.path.isabs(relative_to)):
             filename = os.path.join(os.path.dirname(relative_to), filename)
+
         filename = os.path.normpath(filename)
         cachekey = filename
 
@@ -181,7 +188,6 @@
             except (KeyError, OSError):
                 pass
 
-            search_path = self.search_path
             isabs = False
 
             if os.path.isabs(filename):
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -303,7 +303,9 @@
 
                     # Make the select() function available in the body of the
                     # match template
+                    selected = [False]
                     def select(path):
+                        selected[0] = True
                         return Stream(content).select(path, namespaces, ctxt)
                     vars = dict(select=select)
 
@@ -320,6 +322,13 @@
                             **vars):
                         yield event
 
+                    # If the match template did not actually call select to
+                    # consume the matched stream, the original events need to
+                    # be consumed here or they'll get appended to the output
+                    if not selected[0]:
+                        for event in content:
+                            pass
+
                     break
 
             else: # no matches
--- a/genshi/template/plugin.py
+++ b/genshi/template/plugin.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006-2007 Edgewall Software
+# Copyright (C) 2006-2008 Edgewall Software
 # Copyright (C) 2006 Matthew Good
 # All rights reserved.
 #
@@ -16,8 +16,6 @@
 CherryPy/Buffet.
 """
 
-from pkg_resources import resource_filename
-
 from genshi.input import ET, HTML, XML
 from genshi.output import DocType
 from genshi.template.base import Template
@@ -91,6 +89,7 @@
         if self.use_package_naming:
             divider = templatename.rfind('.')
             if divider >= 0:
+                from pkg_resources import resource_filename
                 package = templatename[:divider]
                 basename = templatename[divider + 1:] + self.extension
                 templatename = resource_filename(package, basename)
--- a/genshi/template/tests/eval.py
+++ b/genshi/template/tests/eval.py
@@ -18,6 +18,7 @@
 import unittest
 
 from genshi.core import Markup
+from genshi.template.base import Context, _ctxt2dict
 from genshi.template.eval import Expression, Suite, Undefined, UndefinedError, \
                                  UNDEFINED
 
@@ -571,6 +572,12 @@
         suite.execute(data)
         assert 'ifilter' in data
 
+    def test_import_star(self):
+        suite = Suite("from itertools import *")
+        data = Context()
+        suite.execute(_ctxt2dict(data))
+        assert 'ifilter' in data
+
     def test_for(self):
         suite = Suite("""x = []
 for i in range(3):
--- a/genshi/template/tests/interpolation.py
+++ b/genshi/template/tests/interpolation.py
@@ -186,6 +186,11 @@
         self.assertEqual(TEXT, parts[2][0])
         self.assertEqual(' baz', parts[2][1])
 
+    def test_interpolate_triplequoted(self):
+        parts = list(interpolate('${"""foo\nbar"""}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual('"""foo\nbar"""', parts[0][1].source)
+
 
 def suite():
     suite = unittest.TestSuite()
--- a/genshi/template/tests/loader.py
+++ b/genshi/template/tests/loader.py
@@ -261,6 +261,47 @@
             </html>""", tmpl1.generate().render())
         assert 'tmpl2.html' in loader._cache
 
+    def test_abspath_include_caching_without_search_path(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl2.html" />
+            </html>""")
+        finally:
+            file1.close()
+
+        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
+        try:
+            file2.write("""<div>Included</div>""")
+        finally:
+            file2.close()
+
+        os.mkdir(os.path.join(self.dirname, 'sub'))
+        file3 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w')
+        try:
+            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl2.html" />
+            </html>""")
+        finally:
+            file3.close()
+
+        file4 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
+        try:
+            file4.write("""<div>Included from sub</div>""")
+        finally:
+            file4.close()
+
+        loader = TemplateLoader()
+        tmpl1 = loader.load(os.path.join(self.dirname, 'tmpl1.html'))
+        self.assertEqual("""<html>
+              <div>Included</div>
+            </html>""", tmpl1.generate().render())
+        tmpl2 = loader.load(os.path.join(self.dirname, 'sub', 'tmpl1.html'))
+        self.assertEqual("""<html>
+              <div>Included from sub</div>
+            </html>""", tmpl2.generate().render())
+        assert 'tmpl2.html' not in loader._cache
+
     def test_load_with_default_encoding(self):
         f = open(os.path.join(self.dirname, 'tmpl.html'), 'w')
         try:
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -691,6 +691,46 @@
         finally:
             shutil.rmtree(dirname)
 
+    def test_nested_matches_without_buffering(self):
+        xml = ("""<html xmlns:py="http://genshi.edgewall.org/">
+          <py:match path="body" once="true" buffer="false">
+            <body>
+              ${select('*|text')}
+              And some other stuff...
+            </body>
+          </py:match>
+          <body>
+            <span py:match="span">Foo</span>
+            <span>Bar</span>
+          </body>
+        </html>""")
+        tmpl = MarkupTemplate(xml, filename='test.html')
+        self.assertEqual("""<html>
+            <body>
+              <span>Foo</span>
+              And some other stuff...
+            </body>
+        </html>""", tmpl.generate().render())
+
+    def test_match_without_select(self):
+        # See <http://genshi.edgewall.org/ticket/243>
+        xml = ("""<html xmlns:py="http://genshi.edgewall.org/">
+          <py:match path="body" buffer="false">
+            <body>
+              This replaces the other text.
+            </body>
+          </py:match>
+          <body>
+            This gets replaced.
+          </body>
+        </html>""")
+        tmpl = MarkupTemplate(xml, filename='test.html')
+        self.assertEqual("""<html>
+            <body>
+              This replaces the other text.
+            </body>
+        </html>""", tmpl.generate().render())
+
 
 def suite():
     suite = unittest.TestSuite()
--- a/genshi/tests/builder.py
+++ b/genshi/tests/builder.py
@@ -16,7 +16,7 @@
 import unittest
 
 from genshi.builder import Element, tag
-from genshi.core import Attrs, Stream
+from genshi.core import Attrs, Markup, Stream
 from genshi.input import XML
 
 
@@ -42,6 +42,15 @@
                           (None, -1, -1)),
                          event)
 
+    def test_duplicate_attributes(self):
+        link = tag.a(href='#1', href_='#2')('Bar')
+        bits = iter(link.generate())
+        self.assertEqual((Stream.START,
+                          ('a', Attrs([('href', "#1")])),
+                          (None, -1, -1)), bits.next())
+        self.assertEqual((Stream.TEXT, u'Bar', (None, -1, -1)), bits.next())
+        self.assertEqual((Stream.END, 'a', (None, -1, -1)), bits.next())
+
     def test_stream_as_child(self):
         xml = list(tag.span(XML('<b>Foo</b>')).generate())
         self.assertEqual(5, len(xml))
@@ -51,6 +60,12 @@
         self.assertEqual((Stream.END, 'b'), xml[3][:2])
         self.assertEqual((Stream.END, 'span'), xml[4][:2])
 
+    def test_markup_escape(self):
+        from genshi.core import Markup
+        m = Markup('See %s') % tag.a('genshi',
+                                     href='http://genshi.edgwall.org')
+        self.assertEqual(m, Markup('See <a href="http://genshi.edgwall.org">'
+                                   'genshi</a>'))
 
 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-2007 Edgewall Software
+# Copyright (C) 2006-2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -23,7 +23,7 @@
 
 class XMLSerializerTestCase(unittest.TestCase):
 
-    def test_xml_serialiser_with_decl(self):
+    def test_with_xml_decl(self):
         stream = Stream([(Stream.XML_DECL, ('1.0', None, -1), (None, -1, -1))])
         output = stream.render(XMLSerializer, doctype='xhtml')
         self.assertEqual('<?xml version="1.0"?>\n'
@@ -203,6 +203,24 @@
 
 class XHTMLSerializerTestCase(unittest.TestCase):
 
+    def test_xml_decl_dropped(self):
+        stream = Stream([(Stream.XML_DECL, ('1.0', None, -1), (None, -1, -1))])
+        output = stream.render(XHTMLSerializer, doctype='xhtml')
+        self.assertEqual('<!DOCTYPE html PUBLIC '
+                         '"-//W3C//DTD XHTML 1.0 Strict//EN" '
+                         '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n',
+                         output)
+
+    def test_xml_decl_included(self):
+        stream = Stream([(Stream.XML_DECL, ('1.0', None, -1), (None, -1, -1))])
+        output = stream.render(XHTMLSerializer, doctype='xhtml',
+                               drop_xml_decl=False)
+        self.assertEqual('<?xml version="1.0"?>\n'
+                         '<!DOCTYPE html PUBLIC '
+                         '"-//W3C//DTD XHTML 1.0 Strict//EN" '
+                         '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n',
+                         output)
+
     def test_xml_lang(self):
         text = '<p xml:lang="en">English text</p>'
         output = XML(text).render(XHTMLSerializer)
@@ -372,7 +390,7 @@
 
     def test_empty_script(self):
         text = '<script src="foo.js" />'
-        output = XML(text).render(XHTMLSerializer)
+        output = XML(text).render(HTMLSerializer)
         self.assertEqual('<script src="foo.js"></script>', output)
 
     def test_script_escaping(self):
--- a/setup.py
+++ b/setup.py
@@ -67,7 +67,7 @@
 
 setup(
     name = 'Genshi',
-    version = '0.5',
+    version = '0.5.2',
     description = 'A toolkit for generation of output for the web',
     long_description = \
 """Genshi is a Python library that provides an integrated set of
Copyright (C) 2012-2017 Edgewall Software