changeset 790:1b6968d31089

Merged the custom-directives branch back into trunk.
author cmlenz
date Wed, 10 Sep 2008 20:53:09 +0000
parents 09531799bac2
children 5ea54d8dfcf5
files genshi/filters/i18n.py genshi/filters/tests/i18n.py genshi/template/base.py genshi/template/markup.py genshi/template/tests/directives.py genshi/template/tests/markup.py genshi/template/text.py
diffstat 7 files changed, 357 insertions(+), 165 deletions(-) [+]
line wrap: on
line diff
--- a/genshi/filters/i18n.py
+++ b/genshi/filters/i18n.py
@@ -23,7 +23,8 @@
 
 from genshi.core import Attrs, Namespace, QName, START, END, TEXT, START_NS, \
                         END_NS, XML_NAMESPACE, _ensure
-from genshi.template.base import Template, EXPR, SUB
+from genshi.template.base import DirectiveFactory, EXPR, SUB, _apply_directives
+from genshi.template.directives import Directive
 from genshi.template.markup import MarkupTemplate, EXEC
 
 __all__ = ['Translator', 'extract']
@@ -32,7 +33,42 @@
 I18N_NAMESPACE = Namespace('http://genshi.edgewall.org/i18n')
 
 
-class Translator(object):
+class CommentDirective(Directive):
+
+    __slots__ = []
+
+    @classmethod
+    def attach(cls, template, stream, value, namespaces, pos):
+        return None, stream
+
+
+class MsgDirective(Directive):
+
+    __slots__ = ['params']
+
+    def __init__(self, value, template, hints=None, namespaces=None,
+                 lineno=-1, offset=-1):
+        Directive.__init__(self, None, template, namespaces, lineno, offset)
+        self.params = [name.strip() for name in value.split(',')]
+
+    def __call__(self, stream, directives, ctxt, **vars):
+        msgbuf = MessageBuffer(self.params)
+
+        stream = iter(stream)
+        yield stream.next() # the outer start tag
+        previous = stream.next()
+        for event in stream:
+            msgbuf.append(*previous)
+            previous = event
+
+        gettext = ctxt.get('_i18n.gettext')
+        for event in msgbuf.translate(gettext(msgbuf.format())):
+            yield event
+
+        yield previous # the outer end tag
+
+
+class Translator(DirectiveFactory):
     """Can extract and translate localizable strings from markup streams and
     templates.
     
@@ -85,12 +121,18 @@
     exclude specific parts of a template from being extracted and translated.
     """
 
+    directives = [
+        ('comment', CommentDirective),
+        ('msg', MsgDirective)
+    ]
+
     IGNORE_TAGS = frozenset([
         QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
         QName('style'), QName('http://www.w3.org/1999/xhtml}style')
     ])
     INCLUDE_ATTRS = frozenset(['abbr', 'alt', 'label', 'prompt', 'standby',
                                'summary', 'title'])
+    NAMESPACE = I18N_NAMESPACE
 
     def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS,
                  include_attrs=INCLUDE_ATTRS, extract_text=True):
@@ -113,7 +155,7 @@
         self.include_attrs = include_attrs
         self.extract_text = extract_text
 
-    def __call__(self, stream, ctxt=None, search_text=True, msgbuf=None):
+    def __call__(self, stream, ctxt=None, search_text=True):
         """Translate any localizable strings in the given stream.
         
         This function shouldn't be called directly. Instead, an instance of
@@ -126,23 +168,23 @@
         :param ctxt: the template context (not used)
         :param search_text: whether text nodes should be translated (used
                             internally)
-        :param msgbuf: a `MessageBuffer` object or `None` (used internally)
         :return: the localized stream
         """
         ignore_tags = self.ignore_tags
         include_attrs = self.include_attrs
+        skip = 0
+        xml_lang = XML_NAMESPACE['lang']
+
         if type(self.translate) is FunctionType:
             gettext = self.translate
         else:
             gettext = self.translate.ugettext
-        if not self.extract_text:
-            search_text = False
+        if ctxt:
+            ctxt['_i18n.gettext'] = gettext
 
-        ns_prefixes = []
-        skip = 0
-        i18n_comment = I18N_NAMESPACE['comment']
-        i18n_msg = I18N_NAMESPACE['msg']
-        xml_lang = XML_NAMESPACE['lang']
+        extract_text = self.extract_text
+        if not extract_text:
+            search_text = False
 
         for kind, data, pos in stream:
 
@@ -168,7 +210,7 @@
                 changed = False
                 for name, value in attrs:
                     newval = value
-                    if search_text and isinstance(value, basestring):
+                    if extract_text and isinstance(value, basestring):
                         if name in include_attrs:
                             newval = gettext(value)
                     else:
@@ -182,48 +224,23 @@
                 if changed:
                     attrs = Attrs(new_attrs)
 
-                if msgbuf:
-                    msgbuf.append(kind, data, pos)
-                    continue
-                elif i18n_msg in attrs:
-                    params = attrs.get(i18n_msg)
-                    if params and type(params) is list: # event tuple
-                        params = params[0][1]
-                    msgbuf = MessageBuffer(params)
-                attrs -= (i18n_comment, i18n_msg)
-
                 yield kind, (tag, attrs), pos
 
             elif search_text and kind is TEXT:
-                if not msgbuf:
-                    text = data.strip()
-                    if text:
-                        data = data.replace(text, unicode(gettext(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:
-                    for event in msgbuf.translate(gettext(msgbuf.format())):
-                        yield event
-                    msgbuf = None
-                    yield kind, data, pos
+                text = data.strip()
+                if text:
+                    data = data.replace(text, unicode(gettext(text)))
+                yield kind, data, pos
 
             elif kind is SUB:
-                subkind, substream = data
-                new_substream = list(self(substream, ctxt, msgbuf=msgbuf))
-                yield kind, (subkind, new_substream), pos
-
-            elif kind is START_NS and data[1] == I18N_NAMESPACE:
-                ns_prefixes.append(data[0])
-
-            elif kind is END_NS and data in ns_prefixes:
-                ns_prefixes.remove(data)
+                directives, substream = data
+                # If this is an i18n:msg directive, no need to translate text
+                # nodes here
+                is_msg = filter(None, [isinstance(d, MsgDirective)
+                                       for d in directives])
+                substream = list(self(substream, ctxt,
+                                      search_text=not is_msg))
+                yield kind, (directives, substream), pos
 
             else:
                 yield kind, data, pos
@@ -372,7 +389,9 @@
         :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(',')]
+        if isinstance(params, basestring):
+            params = [name.strip() for name in params.split(',')]
+        self.params = params
         self.comment = comment
         self.lineno = lineno
         self.string = []
--- a/genshi/filters/tests/i18n.py
+++ b/genshi/filters/tests/i18n.py
@@ -174,7 +174,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"Für Details siehe bitte [1:Hilfe]."
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p>Für Details siehe bitte <a href="help.html">Hilfe</a>.</p>
         </html>""", tmpl.generate().render())
@@ -200,7 +202,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"Für Details siehe bitte [1:[2:Hilfeseite]]."
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p>Für Details siehe bitte <a href="help.html"><em>Hilfeseite</em></a>.</p>
         </html>""", tmpl.generate().render())
@@ -225,7 +229,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"[1:] Einträge pro Seite anzeigen."
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p><input type="text" name="num"/> Einträge pro Seite anzeigen.</p>
         </html>""", tmpl.generate().render())
@@ -250,7 +256,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"Für [2:Details] siehe bitte [1:Hilfe]."
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p>Für <em>Details</em> siehe bitte <a href="help.html">Hilfe</a>.</p>
         </html>""", tmpl.generate().render())
@@ -276,7 +284,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"[1:] Einträge pro Seite, beginnend auf Seite [2:]."
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p><input type="text" name="num"/> Eintr\xc3\xa4ge pro Seite, beginnend auf Seite <input type="text" name="num"/>.</p>
         </html>""", tmpl.generate().render())
@@ -301,7 +311,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"Hallo, %(name)s!"
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p>Hallo, Jim!</p>
         </html>""", tmpl.generate(user=dict(name='Jim')).render())
@@ -314,7 +326,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"%(name)s, sei gegrüßt!"
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p>Jim, sei gegrüßt!</p>
         </html>""", tmpl.generate(user=dict(name='Jim')).render())
@@ -327,7 +341,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"Sei gegrüßt, [1:Alter]!"
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p>Sei gegrüßt, <a href="#42">Alter</a>!</p>
         </html>""", tmpl.generate(anchor='42').render())
@@ -352,7 +368,9 @@
           </p>
         </html>""")
         gettext = lambda s: u"%(name)s schrieb dies um %(time)s"
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         entry = {
             'author': 'Jim',
             'time': datetime(2008, 4, 1, 14, 30)
@@ -403,18 +421,48 @@
           <p i18n:msg="" i18n:comment="As in foo bar">Foo</p>
         </html>""")
         gettext = lambda s: u"Voh"
-        tmpl.filters.insert(0, Translator(gettext))
+        translator = Translator(gettext)
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p>Voh</p>
         </html>""", tmpl.generate().render())
 
+    def test_extract_i18n_msg_with_attr(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" title="Foo bar">Foo</p>
+        </html>""")
+        translator = Translator()
+        messages = list(translator.extract(tmpl.stream))
+        self.assertEqual(2, len(messages))
+        self.assertEqual((3, None, u'Foo bar', []), messages[0])
+        self.assertEqual((3, None, u'Foo', []), messages[1])
+
+    def test_translate_i18n_msg_with_attr(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" title="Foo bar">Foo</p>
+        </html>""")
+        gettext = lambda s: u"Voh"
+        translator = Translator(DummyTranslations({
+            'Foo': u'Voh',
+            'Foo bar': u'Voh bär'
+        }))
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
+        self.assertEqual("""<html>
+          <p title="Voh bär">Voh</p>
+        </html>""", tmpl.generate().render())
+
     def test_translate_with_translations_object(self):
         tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
             xmlns:i18n="http://genshi.edgewall.org/i18n">
           <p i18n:msg="" i18n:comment="As in foo bar">Foo</p>
         </html>""")
-        translations = DummyTranslations({'Foo': 'Voh'})
-        tmpl.filters.insert(0, Translator(translations))
+        translator = Translator(DummyTranslations({'Foo': 'Voh'}))
+        tmpl.filters.insert(0, translator)
+        tmpl.add_directives(Translator.NAMESPACE, translator)
         self.assertEqual("""<html>
           <p>Voh</p>
         </html>""", tmpl.generate().render())
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -26,8 +26,8 @@
 from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
 from genshi.input import ParseError
 
-__all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError',
-           'TemplateSyntaxError', 'BadDirectiveError']
+__all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError',
+           'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError']
 __docformat__ = 'restructuredtext en'
 
 
@@ -301,8 +301,8 @@
         ctxt.frames[0].update(top)
 
 
-class TemplateMeta(type):
-    """Meta class for templates."""
+class DirectiveFactoryMeta(type):
+    """Meta class for directive factories."""
 
     def __new__(cls, name, bases, d):
         if 'directives' in d:
@@ -312,13 +312,44 @@
         return type.__new__(cls, name, bases, d)
 
 
-class Template(object):
+class DirectiveFactory(object):
+    """Base for classes that provide a set of template directives.
+    
+    :since: version 0.6
+    """
+    __metaclass__ = DirectiveFactoryMeta
+
+    directives = []
+    """A list of `(name, cls)` tuples that define the set of directives
+    provided by this factory.
+    """
+
+    def compare_directives(self):
+        """Return a function that takes two directive classes and compares
+        them to determine their relative ordering.
+        """
+        def _get_index(cls):
+            if cls in self._dir_order:
+                return self._dir_order.index(cls)
+            return 0
+        return lambda a, b: cmp(_get_index(a[0]), _get_index(b[0]))
+
+    def get_directive(self, name):
+        """Return the directive class for the given name.
+        
+        :param name: the directive name as used in the template
+        :return: the directive class
+        :see: `Directive`
+        """
+        return self._dir_by_name.get(name)
+
+
+class Template(DirectiveFactory):
     """Abstract template base class.
     
     This class implements most of the template processing model, but does not
     specify the syntax of templates.
     """
-    __metaclass__ = TemplateMeta
 
     EXEC = StreamEventKind('EXEC')
     """Stream event kind representing a Python code suite to execute."""
@@ -363,13 +394,14 @@
         self.lookup = lookup
         self.allow_exec = allow_exec
         self._init_filters()
+        self._prepared = False
 
         if isinstance(source, basestring):
             source = StringIO(source)
         else:
             source = source
         try:
-            self.stream = list(self._prepare(self._parse(source, encoding)))
+            self._stream = self._parse(source, encoding)
         except ParseError, e:
             raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
 
@@ -390,6 +422,13 @@
         if self.loader:
             self.filters.append(self._include)
 
+    def _get_stream(self):
+        if not self._prepared:
+            self._stream = list(self._prepare(self._stream))
+            self._prepared = True
+        return self._stream
+    stream = property(_get_stream)
+
     def _parse(self, source, encoding):
         """Parse the template.
         
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -42,8 +42,8 @@
     </ul>
     """
 
-    DIRECTIVE_NAMESPACE = Namespace('http://genshi.edgewall.org/')
-    XINCLUDE_NAMESPACE = Namespace('http://www.w3.org/2001/XInclude')
+    DIRECTIVE_NAMESPACE = 'http://genshi.edgewall.org/'
+    XINCLUDE_NAMESPACE = 'http://www.w3.org/2001/XInclude'
 
     directives = [('def', DefDirective),
                   ('match', MatchDirective),
@@ -60,6 +60,13 @@
     serializer = 'xml'
     _number_conv = Markup
 
+    def __init__(self, source, filepath=None, filename=None, loader=None,
+                 encoding=None, lookup='strict', allow_exec=True):
+        Template.__init__(self, source, filepath=filepath, filename=filename,
+                          loader=loader, encoding=encoding, lookup=lookup,
+                          allow_exec=allow_exec)
+        self.add_directives(self.DIRECTIVE_NAMESPACE, self)
+
     def _init_filters(self):
         Template._init_filters(self)
         # Make sure the include filter comes after the match filter
@@ -70,45 +77,58 @@
             self.filters.append(self._include)
 
     def _parse(self, source, encoding):
-        streams = [[]] # stacked lists of events of the "compiled" template
-        dirmap = {} # temporary mapping of directives to elements
-        ns_prefix = {}
-        depth = 0
-        fallbacks = []
-        includes = []
-
         if not isinstance(source, Stream):
             source = XMLParser(source, filename=self.filename,
                                encoding=encoding)
+        stream = []
 
         for kind, data, pos in source:
-            stream = streams[-1]
 
-            if kind is START_NS:
-                # Strip out the namespace declaration for template directives
-                prefix, uri = data
-                ns_prefix[prefix] = uri
-                if uri not in (self.DIRECTIVE_NAMESPACE,
-                               self.XINCLUDE_NAMESPACE):
+            if kind is TEXT:
+                for kind, data, pos in interpolate(data, self.filepath, pos[1],
+                                                   pos[2], lookup=self.lookup):
                     stream.append((kind, data, pos))
 
-            elif kind is END_NS:
-                uri = ns_prefix.pop(data, None)
-                if uri and uri not in (self.DIRECTIVE_NAMESPACE,
-                                       self.XINCLUDE_NAMESPACE):
+            elif kind is PI and data[0] == 'python':
+                if not self.allow_exec:
+                    raise TemplateSyntaxError('Python code blocks not allowed',
+                                              self.filepath, *pos[1:])
+                try:
+                    suite = Suite(data[1], self.filepath, pos[1],
+                                  lookup=self.lookup)
+                except SyntaxError, err:
+                    raise TemplateSyntaxError(err, self.filepath,
+                                              pos[1] + (err.lineno or 1) - 1,
+                                              pos[2] + (err.offset or 0))
+                stream.append((EXEC, suite, pos))
+
+            elif kind is COMMENT:
+                if not data.lstrip().startswith('!'):
                     stream.append((kind, data, pos))
 
-            elif kind is START:
-                # Record any directive attributes in start tags
+            else:
+                stream.append((kind, data, pos))
+
+        return stream
+
+    def _extract_directives(self, stream, namespace, factory):
+        depth = 0
+        dirmap = {} # temporary mapping of directives to elements
+        new_stream = []
+        ns_prefix = {} # namespace prefixes in use
+
+        for kind, data, pos in stream:
+
+            if kind is START:
                 tag, attrs = data
                 directives = []
                 strip = False
 
-                if tag in self.DIRECTIVE_NAMESPACE:
-                    cls = self._dir_by_name.get(tag.localname)
+                if tag.namespace == namespace:
+                    cls = factory.get_directive(tag.localname)
                     if cls is None:
-                        raise BadDirectiveError(tag.localname, self.filepath,
-                                                pos[1])
+                        raise BadDirectiveError(tag.localname,
+                                                self.filepath, pos[1])
                     args = dict([(name.localname, value) for name, value
                                  in attrs if not name.namespace])
                     directives.append((cls, args, ns_prefix.copy(), pos))
@@ -116,53 +136,104 @@
 
                 new_attrs = []
                 for name, value in attrs:
-                    if name in self.DIRECTIVE_NAMESPACE:
-                        cls = self._dir_by_name.get(name.localname)
+                    if name.namespace == namespace:
+                        cls = factory.get_directive(name.localname)
                         if cls is None:
                             raise BadDirectiveError(name.localname,
                                                     self.filepath, pos[1])
-                        directives.append((cls, value, ns_prefix.copy(), pos))
+                        if type(value) is list and len(value) == 1:
+                            value = value[0][1]
+                        directives.append((cls, value, ns_prefix.copy(),
+                                           pos))
                     else:
-                        if value:
-                            value = list(interpolate(value, self.filepath,
-                                                     pos[1], pos[2],
-                                                     lookup=self.lookup))
-                            if len(value) == 1 and value[0][0] is TEXT:
-                                value = value[0][1]
-                        else:
-                            value = [(TEXT, u'', pos)]
                         new_attrs.append((name, value))
                 new_attrs = Attrs(new_attrs)
 
                 if directives:
-                    index = self._dir_order.index
-                    directives.sort(lambda a, b: cmp(index(a[0]), index(b[0])))
-                    dirmap[(depth, tag)] = (directives, len(stream), strip)
+                    directives.sort(self.compare_directives())
+                    dirmap[(depth, tag)] = (directives, len(new_stream),
+                                            strip)
 
-                if tag in self.XINCLUDE_NAMESPACE:
+                new_stream.append((kind, (tag, new_attrs), pos))
+                depth += 1
+
+            elif kind is END:
+                depth -= 1
+                new_stream.append((kind, data, pos))
+
+                # If there have have directive attributes with the
+                # corresponding start tag, move the events inbetween into
+                # a "subprogram"
+                if (depth, data) in dirmap:
+                    directives, offset, strip = dirmap.pop((depth, data))
+                    substream = new_stream[offset:]
+                    if strip:
+                        substream = substream[1:-1]
+                    new_stream[offset:] = [
+                        (SUB, (directives, substream), pos)
+                    ]
+
+            elif kind is SUB:
+                directives, substream = data
+                substream = self._extract_directives(substream, namespace,
+                                                     factory)
+
+                if len(substream) == 1 and substream[0][0] is SUB:
+                    added_directives, substream = substream[0][1]
+                    directives += added_directives
+
+                new_stream.append((kind, (directives, substream), pos))
+
+            elif kind is START_NS:
+                # Strip out the namespace declaration for template
+                # directives
+                prefix, uri = data
+                ns_prefix[prefix] = uri
+                if uri != namespace:
+                    new_stream.append((kind, data, pos))
+
+            elif kind is END_NS:
+                uri = ns_prefix.pop(data, None)
+                if uri and uri != namespace:
+                    new_stream.append((kind, data, pos))
+
+            else:
+                new_stream.append((kind, data, pos))
+
+        return new_stream
+
+    def _extract_includes(self, stream):
+        streams = [[]] # stacked lists of events of the "compiled" template
+        prefixes = {}
+        fallbacks = []
+        includes = []
+        xinclude_ns = Namespace(self.XINCLUDE_NAMESPACE)
+
+        for kind, data, pos in stream:
+            stream = streams[-1]
+
+            if kind is START:
+                # Record any directive attributes in start tags
+                tag, attrs = data
+                if tag in xinclude_ns:
                     if tag.localname == 'include':
-                        include_href = new_attrs.get('href')
+                        include_href = attrs.get('href')
                         if not include_href:
                             raise TemplateSyntaxError('Include misses required '
                                                       'attribute "href"',
                                                       self.filepath, *pos[1:])
-                        includes.append((include_href, new_attrs.get('parse')))
+                        includes.append((include_href, attrs.get('parse')))
                         streams.append([])
                     elif tag.localname == 'fallback':
                         streams.append([])
                         fallbacks.append(streams[-1])
-
                 else:
-                    stream.append((kind, (tag, new_attrs), pos))
-
-                depth += 1
+                    stream.append((kind, (tag, attrs), pos))
 
             elif kind is END:
-                depth -= 1
-
-                if fallbacks and data == self.XINCLUDE_NAMESPACE['fallback']:
+                if fallbacks and data == xinclude_ns['fallback']:
                     assert streams.pop() is fallbacks[-1]
-                elif data == self.XINCLUDE_NAMESPACE['include']:
+                elif data == xinclude_ns['include']:
                     fallback = None
                     if len(fallbacks) == len(includes):
                         fallback = fallbacks.pop()
@@ -183,37 +254,12 @@
                 else:
                     stream.append((kind, data, pos))
 
-                # If there have have directive attributes with the corresponding
-                # start tag, move the events inbetween into a "subprogram"
-                if (depth, data) in dirmap:
-                    directives, start_offset, strip = dirmap.pop((depth, data))
-                    substream = stream[start_offset:]
-                    if strip:
-                        substream = substream[1:-1]
-                    stream[start_offset:] = [(SUB, (directives, substream),
-                                              pos)]
+            elif kind is START_NS and data[1] == xinclude_ns:
+                # Strip out the XInclude namespace
+                prefixes[data[0]] = data[1]
 
-            elif kind is PI and data[0] == 'python':
-                if not self.allow_exec:
-                    raise TemplateSyntaxError('Python code blocks not allowed',
-                                              self.filepath, *pos[1:])
-                try:
-                    suite = Suite(data[1], self.filepath, pos[1],
-                                  lookup=self.lookup)
-                except SyntaxError, err:
-                    raise TemplateSyntaxError(err, self.filepath,
-                                              pos[1] + (err.lineno or 1) - 1,
-                                              pos[2] + (err.offset or 0))
-                stream.append((EXEC, suite, pos))
-
-            elif kind is TEXT:
-                for kind, data, pos in interpolate(data, self.filepath, pos[1],
-                                                   pos[2], lookup=self.lookup):
-                    stream.append((kind, data, pos))
-
-            elif kind is COMMENT:
-                if not data.lstrip().startswith('!'):
-                    stream.append((kind, data, pos))
+            elif kind is END_NS and data in prefixes:
+                prefixes.pop(data)
 
             else:
                 stream.append((kind, data, pos))
@@ -221,6 +267,45 @@
         assert len(streams) == 1
         return streams[0]
 
+    def _interpolate_attrs(self, stream):
+        for kind, data, pos in stream:
+
+            if kind is START:
+                # Record any directive attributes in start tags
+                tag, attrs = data
+                new_attrs = []
+                for name, value in attrs:
+                    if value:
+                        value = list(interpolate(value, self.filepath, pos[1],
+                                                 pos[2], lookup=self.lookup))
+                        if len(value) == 1 and value[0][0] is TEXT:
+                            value = value[0][1]
+                    else:
+                        value = [(TEXT, u'', pos)]
+                    new_attrs.append((name, value))
+                data = tag, Attrs(new_attrs)
+
+            yield kind, data, pos
+
+    def _prepare(self, stream):
+        return Template._prepare(self,
+            self._extract_includes(self._interpolate_attrs(stream))
+        )
+
+    def add_directives(self, namespace, factory):
+        """Register a custom `DirectiveFactory` for a given namespace.
+        
+        :param namespace: the namespace URI
+        :type namespace: `basestring`
+        :param factory: the directive factory to register
+        :type factory: `DirectiveFactory`
+        :since: version 0.6
+        """
+        assert not self._prepared, 'Too late for adding directives, ' \
+                                   'template already prepared'
+        self._stream = self._extract_directives(self._stream, namespace,
+                                                factory)
+
     def _match(self, stream, ctxt, start=0, end=None, **vars):
         """Internal stream filter that applies any defined match templates
         to the stream.
--- a/genshi/template/tests/directives.py
+++ b/genshi/template/tests/directives.py
@@ -504,10 +504,10 @@
         """
         try:
             MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:for each="">
-            empty
-          </py:for>
-        </doc>""", filename='test.html')
+              <py:for each="">
+                empty
+              </py:for>
+            </doc>""", filename='test.html').generate()
             self.fail('ExpectedTemplateSyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
@@ -963,9 +963,9 @@
 
     def test_as_element(self):
         try:
-            tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+            MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
               <py:content foo="">Foo</py:content>
-            </doc>""", filename='test.html')
+            </doc>""", filename='test.html').generate()
             self.fail('Expected TemplateSyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
@@ -981,9 +981,9 @@
         expression is supplied.
         """
         try:
-            tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+            MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
               <elem py:replace="">Foo</elem>
-            </doc>""", filename='test.html')
+            </doc>""", filename='test.html').generate()
             self.fail('Expected TemplateSyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -86,8 +86,8 @@
     def test_directive_value_syntax_error(self):
         xml = """<p xmlns:py="http://genshi.edgewall.org/" py:if="bar'" />"""
         try:
-            tmpl = MarkupTemplate(xml, filename='test.html')
-            self.fail('Expected SyntaxError')
+            tmpl = MarkupTemplate(xml, filename='test.html').generate()
+            self.fail('Expected TemplateSyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
             self.assertEqual(1, e.lineno)
@@ -98,7 +98,7 @@
         </p>"""
         try:
             tmpl = MarkupTemplate(xml, filename='test.html')
-            self.fail('Expected SyntaxError')
+            self.fail('Expected TemplateSyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
             self.assertEqual(2, e.lineno)
@@ -111,7 +111,7 @@
         </p>"""
         try:
             tmpl = MarkupTemplate(xml, filename='test.html')
-            self.fail('Expected SyntaxError')
+            self.fail('Expected TemplateSyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
             self.assertEqual(3, e.lineno)
@@ -130,7 +130,8 @@
 
     def test_text_noescape_quotes(self):
         """
-        Verify that outputting context data in text nodes doesn't escape quotes.
+        Verify that outputting context data in text nodes doesn't escape
+        quotes.
         """
         tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           $myvar
--- a/genshi/template/text.py
+++ b/genshi/template/text.py
@@ -216,7 +216,7 @@
                                               (self.filepath, lineno, 0))]
 
             elif command:
-                cls = self._dir_by_name.get(command)
+                cls = self.get_directive(command)
                 if cls is None:
                     raise BadDirectiveError(command)
                 directive = cls, value, None, (self.filepath, lineno, 0)
@@ -312,7 +312,7 @@
                 pos = (self.filename, lineno, 0)
                 stream.append((INCLUDE, (value.strip(), None, []), pos))
             elif command != '#':
-                cls = self._dir_by_name.get(command)
+                cls = self.get_directive(command)
                 if cls is None:
                     raise BadDirectiveError(command)
                 directive = cls, value, None, (self.filepath, lineno, 0)
Copyright (C) 2012-2017 Edgewall Software