changeset 355:94639584725a experimental-inline

inline branch: Merged [430:434/trunk].
author cmlenz
date Mon, 13 Nov 2006 18:16:57 +0000
parents 4a2050e9dcd8
children cc04c70d1bbd
files doc/text-templates.txt doc/xml-templates.txt genshi/template/core.py genshi/template/directives.py genshi/template/inline.py genshi/template/plugin.py genshi/template/tests/__init__.py genshi/template/tests/plugin.py genshi/template/tests/templates/__init__.py genshi/template/tests/templates/functions.html genshi/template/tests/templates/functions.txt genshi/template/tests/templates/test.html genshi/template/tests/templates/test.txt
diffstat 13 files changed, 337 insertions(+), 81 deletions(-) [+]
line wrap: on
line diff
--- a/doc/text-templates.txt
+++ b/doc/text-templates.txt
@@ -116,7 +116,8 @@
 Template Directives
 -------------------
 
-Directives are lines starting with a ``#`` character followed immediately by the directive name. They can affect how the template is rendered in a number of
+Directives are lines starting with a ``#`` character followed immediately by
+the directive name. They can affect how the template is rendered in a number of
 ways: Genshi provides directives for conditionals and looping, among others.
 
 Directives must be on separate lines, and the ``#`` character must be be the
--- a/doc/xml-templates.txt
+++ b/doc/xml-templates.txt
@@ -104,7 +104,7 @@
 You **will** however get a ``NameError`` if you try to call an undefined 
 variable, or do anything else with it, such as accessing its attributes. If you
 need to know whether a variable is defined, you can check its type against the
-``Undefined`` class, for example in an `py:if`_ directive::
+``Undefined`` class, for example in a `py:if`_ directive::
 
   >>> from genshi.template import TextTemplate
   >>> tmpl = TextTemplate('${type(doh) is Undefined}')
--- a/genshi/template/core.py
+++ b/genshi/template/core.py
@@ -178,6 +178,15 @@
             expr = ' "%s"' % self.expr.source
         return '<%s%s>' % (self.__class__.__name__, expr)
 
+    def prepare(self, directives, stream):
+        """Called after the template stream has been completely parsed.
+        
+        The part of the template stream associated with the directive will be
+        replaced by what this function returns. This allows the directive to
+        optimize the template or validate the way the directive is used.
+        """
+        return stream
+
     def tagname(self):
         """Return the local tag name of the directive as it is used in
         templates.
@@ -231,7 +240,7 @@
 
         self.filters = [self._flatten, self._eval]
 
-        self.stream = self._parse(encoding)
+        self.stream = list(self._prepare(self._parse(encoding)))
 
     def __repr__(self):
         return '<%s "%s">' % (self.__class__.__name__, self.filename)
@@ -289,6 +298,25 @@
         return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE])
     _interpolate = classmethod(_interpolate)
 
+    def _prepare(self, stream):
+        """Call the `prepare` method of every directive instance in the
+        template so that various optimization and validation tasks can be
+        performed.
+        """
+        for kind, data, pos in stream:
+            if kind is SUB:
+                directives, substream = data
+                for directive in directives[:]:
+                    substream = directive.prepare(directives, substream)
+                substream = self._prepare(substream)
+                if directives:
+                    yield kind, (directives, list(substream)), pos
+                else:
+                    for event in substream:
+                        yield event
+            else:
+                yield kind, data, pos
+
     def compile(self):
         """Compile the template to a Python module, and return the module
         object.
--- a/genshi/template/directives.py
+++ b/genshi/template/directives.py
@@ -114,16 +114,9 @@
     """
     __slots__ = []
 
-    def __call__(self, stream, ctxt, directives):
-        def _generate():
-            yield stream.next()
-            yield EXPR, self.expr, (None, -1, -1)
-            event = stream.next()
-            for next in stream:
-                event = next
-            yield event
-
-        return _apply_directives(_generate(), ctxt, directives)
+    def prepare(self, directives, stream):
+        directives.remove(self)
+        return [stream[0], (EXPR, self.expr, (None, -1, --1)),  stream[-1]]
 
 
 class DefDirective(Directive):
@@ -368,8 +361,9 @@
     """
     __slots__ = []
 
-    def __call__(self, stream, ctxt, directives):
-        yield EXPR, self.expr, (None, -1, -1)
+    def prepare(self, directives, stream):
+        directives.remove(self)
+        return [(EXPR, self.expr, (None, -1, -1))]
 
 
 class StripDirective(Directive):
@@ -407,11 +401,7 @@
 
     def __call__(self, stream, ctxt, directives):
         def _generate():
-            if self.expr:
-                strip = self.expr.evaluate(ctxt)
-            else:
-                strip = True
-            if strip:
+            if self.expr.evaluate(ctxt):
                 stream.next() # skip start tag
                 previous = stream.next()
                 for event in stream:
@@ -420,8 +410,13 @@
             else:
                 for event in stream:
                     yield event
+        return _apply_directives(_generate(), ctxt, directives)
 
-        return _apply_directives(_generate(), ctxt, directives)
+    def prepare(self, directives, stream):
+        if not self.expr:
+            directives.remove(self)
+            return stream[1:-1]
+        return stream
 
 
 class ChooseDirective(Directive):
--- a/genshi/template/inline.py
+++ b/genshi/template/inline.py
@@ -174,23 +174,7 @@
         yield w()
         yield w('# Applying %r', directive)
 
-        if isinstance(directive, ContentDirective):
-            ei[0] += 1
-            yield w('for e in _expand(E%d.evaluate(ctxt), %r):',
-                    p_exprs[directive.expr], (None, -1, -1))
-            w.shift()
-            lines = _apply(directives, stream)
-            for line in lines:
-                yield line
-                break
-            yield w('yield e')
-            line = lines.next()
-            for next in lines:
-                line = next
-            yield line
-            w.unshift()
-
-        elif isinstance(directive, DefDirective):
+        if isinstance(directive, DefDirective):
             pass
 
         elif isinstance(directive, ForDirective):
@@ -211,12 +195,6 @@
                 yield line
             w.unshift()
 
-        elif isinstance(directive, ReplaceDirective):
-            ei[0] += 1
-            yield w('for e in _expand(E%d.evaluate(ctxt), %r): yield e',
-                    p_exprs[directive.expr],
-                    (None, -1, -1))
-
         elif isinstance(directive, WithDirective):
             for targets, expr in directive.vars:
                 ei[0] += 1
@@ -228,28 +206,19 @@
             yield w('ctxt.pop()')
 
         elif isinstance(directive, StripDirective):
-            if directive.expr:
-                yield w('if E%d.evaluate(ctxt):', p_exprs[directive.expr])
-                w.shift()
-                lines = _apply(directives, stream)
-                previous = lines.next()
-                for line in lines:
-                    yield previous
-                    previous = line
-                w.unshift()
-                yield w('else:')
-                w.shift()
-                for line in _apply(directives, stream):
-                    yield line
-                w.unshift()
-            else: # always strip
-                lines = _apply(directives, stream)
-                yield w('# stripped %r', lines.next().strip())
-                previous = lines.next()
-                for line in lines:
-                    yield previous
-                    previous = line
-                yield w('# stripped %r', previous.strip())
+            yield w('if E%d.evaluate(ctxt):', p_exprs[directive.expr])
+            w.shift()
+            lines = _apply(directives, stream)
+            previous = lines.next()
+            for line in lines:
+                yield previous
+                previous = line
+            w.unshift()
+            yield w('else:')
+            w.shift()
+            for line in _apply(directives, stream):
+                yield line
+            w.unshift()
 
         else:
             raise NotImplementedError
--- a/genshi/template/plugin.py
+++ b/genshi/template/plugin.py
@@ -18,14 +18,17 @@
 
 from pkg_resources import resource_filename
 
-from genshi.eval import Undefined
 from genshi.input import ET, HTML, XML
 from genshi.output import DocType
 from genshi.template.core import Context, Template
+from genshi.template.eval import Undefined
 from genshi.template.loader import TemplateLoader
 from genshi.template.markup import MarkupTemplate
 from genshi.template.text import TextTemplate
 
+__all__ = ['ConfigurationError', 'MarkupTemplateEnginePlugin',
+           'TextTemplateEnginePlugin']
+
 
 class ConfigurationError(Exception):
     """Exception raised when invalid plugin options are encountered."""
@@ -44,14 +47,15 @@
         self.options = options
 
         self.default_encoding = options.get('genshi.default_encoding', 'utf-8')
-        auto_reload = options.get('genshi.auto_reload', '1').lower() \
-                            in ('1', 'yes', 'true')
+        auto_reload = options.get('genshi.auto_reload', '1')
+        if isinstance(auto_reload, basestring):
+            auto_reload = auto_reload.lower() in ('1', 'on', 'yes', 'true')
         search_path = options.get('genshi.search_path', '').split(':')
         try:
             max_cache_size = int(options.get('genshi.max_cache_size', 25))
         except ValueError:
             raise ConfigurationError('Invalid value for max_cache_size: "%s"' %
-                                     max_cache_size)
+                                     options.get('genshi.max_cache_size'))
 
         self.loader = TemplateLoader(filter(None, search_path),
                                      auto_reload=auto_reload,
@@ -116,12 +120,12 @@
     def __init__(self, extra_vars_func=None, options=None):
         AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
 
-        doctype = options.get('genshi.default_doctype')
+        doctype = self.options.get('genshi.default_doctype')
         if doctype and doctype not in self.doctypes:
             raise ConfigurationError('Unknown doctype "%s"' % doctype)
         self.default_doctype = self.doctypes.get(doctype)
 
-        format = options.get('genshi.default_format', 'html')
+        format = self.options.get('genshi.default_format', 'html')
         if format not in ('html', 'xhtml', 'xml', 'text'):
             raise ConfigurationError('Unknown output format "%s"' % format)
         self.default_format = format
@@ -148,11 +152,3 @@
     template_class = TextTemplate
     extension = '.txt'
     default_format = 'text'
-
-    def transform(self, info, template):
-        """Render the output to an event stream."""
-        data = {}
-        if self.get_extra_vars:
-            data.update(self.get_extra_vars())
-        data.update(info)
-        return super(TextTemplateEnginePlugin, self).transform(data, template)
--- a/genshi/template/tests/__init__.py
+++ b/genshi/template/tests/__init__.py
@@ -17,13 +17,14 @@
 
 def suite():
     from genshi.template.tests import core, directives, eval, loader, markup, \
-                                      text
+                                      plugin, text
     suite = unittest.TestSuite()
     suite.addTest(core.suite())
     suite.addTest(directives.suite())
     suite.addTest(eval.suite())
     suite.addTest(loader.suite())
     suite.addTest(markup.suite())
+    suite.addTest(plugin.suite())
     suite.addTest(text.suite())
     return suite
 
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/plugin.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006 Matthew Good
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+import doctest
+import os
+import unittest
+
+from genshi.core import Stream
+from genshi.output import DocType
+from genshi.template import MarkupTemplate, TextTemplate
+from genshi.template.plugin import ConfigurationError, \
+                                   MarkupTemplateEnginePlugin, \
+                                   TextTemplateEnginePlugin
+
+PACKAGE = 'genshi.template.tests'
+
+
+class MarkupTemplateEnginePluginTestCase(unittest.TestCase):
+
+    def test_init_no_options(self):
+        plugin = MarkupTemplateEnginePlugin()
+        self.assertEqual('utf-8', plugin.default_encoding)
+        self.assertEqual('html', plugin.default_format)
+        self.assertEqual(None, plugin.default_doctype)
+
+        self.assertEqual([], plugin.loader.search_path)
+        self.assertEqual(True, plugin.loader.auto_reload)
+        self.assertEqual(25, plugin.loader._cache.capacity)
+
+    def test_init_with_loader_options(self):
+        plugin = MarkupTemplateEnginePlugin(options={
+            'genshi.auto_reload': 'off',
+            'genshi.max_cache_size': '100',
+            'genshi.search_path': '/usr/share/tmpl:/usr/local/share/tmpl',
+        })
+        self.assertEqual(['/usr/share/tmpl', '/usr/local/share/tmpl'],
+                         plugin.loader.search_path)
+        self.assertEqual(False, plugin.loader.auto_reload)
+        self.assertEqual(100, plugin.loader._cache.capacity)
+
+    def test_init_with_invalid_cache_size(self):
+        self.assertRaises(ConfigurationError, MarkupTemplateEnginePlugin,
+                          options={'genshi.max_cache_size': 'thirty'})
+
+    def test_init_with_output_options(self):
+        plugin = MarkupTemplateEnginePlugin(options={
+            'genshi.default_encoding': 'iso-8859-15',
+            'genshi.default_format': 'xhtml',
+            'genshi.default_doctype': 'xhtml-strict',
+        })
+        self.assertEqual('iso-8859-15', plugin.default_encoding)
+        self.assertEqual('xhtml', plugin.default_format)
+        self.assertEqual(DocType.XHTML, plugin.default_doctype)
+
+    def test_init_with_invalid_output_format(self):
+        self.assertRaises(ConfigurationError, MarkupTemplateEnginePlugin,
+                          options={'genshi.default_format': 'foobar'})
+
+    def test_init_with_invalid_doctype(self):
+        self.assertRaises(ConfigurationError, MarkupTemplateEnginePlugin,
+                          options={'genshi.default_doctype': 'foobar'})
+
+    def test_load_template_from_file(self):
+        plugin = MarkupTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.test')
+        self.assertEqual('test.html', os.path.basename(tmpl.filename))
+        assert isinstance(tmpl, MarkupTemplate)
+
+    def test_load_template_from_string(self):
+        plugin = MarkupTemplateEnginePlugin()
+        tmpl = plugin.load_template(None, template_string="""<p>
+          $message
+        </p>""")
+        self.assertEqual(None, tmpl.filename)
+        assert isinstance(tmpl, MarkupTemplate)
+
+    def test_transform_with_load(self):
+        plugin = MarkupTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.test')
+        stream = plugin.transform({'message': 'Hello'}, tmpl)
+        assert isinstance(stream, Stream)
+
+    def test_transform_without_load(self):
+        plugin = MarkupTemplateEnginePlugin()
+        stream = plugin.transform({'message': 'Hello'},
+                                  PACKAGE + '.templates.test')
+        assert isinstance(stream, Stream)
+
+    def test_render(self):
+        plugin = MarkupTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.test')
+        output = plugin.render({'message': 'Hello'}, template=tmpl)
+        self.assertEqual("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html lang="en">
+  <head>
+    <title>Test</title>
+  </head>
+  <body>
+    <h1>Test</h1>
+    <p>Hello</p>
+  </body>
+</html>""", output)
+
+    def test_render_with_format(self):
+        plugin = MarkupTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.test')
+        output = plugin.render({'message': 'Hello'}, format='xhtml',
+                               template=tmpl)
+        self.assertEqual("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>Test</title>
+  </head>
+  <body>
+    <h1>Test</h1>
+    <p>Hello</p>
+  </body>
+</html>""", output)
+
+    def test_render_with_doctype(self):
+        plugin = MarkupTemplateEnginePlugin(options={
+            'genshi.default_doctype': 'html-strict',
+        })
+        tmpl = plugin.load_template(PACKAGE + '.templates.test')
+        output = plugin.render({'message': 'Hello'}, template=tmpl)
+        self.assertEqual("""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html lang="en">
+  <head>
+    <title>Test</title>
+  </head>
+  <body>
+    <h1>Test</h1>
+    <p>Hello</p>
+  </body>
+</html>""", output)
+
+    def test_helper_functions(self):
+        plugin = MarkupTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.functions')
+        output = plugin.render({'snippet': '<b>Foo</b>'}, template=tmpl)
+        self.assertEqual("""<div>
+False
+bar
+<b>Foo</b>
+<b>Foo</b>
+</div>""", output)
+
+
+class TextTemplateEnginePluginTestCase(unittest.TestCase):
+
+    def test_init_no_options(self):
+        plugin = TextTemplateEnginePlugin()
+        self.assertEqual('utf-8', plugin.default_encoding)
+        self.assertEqual('text', plugin.default_format)
+
+        self.assertEqual([], plugin.loader.search_path)
+        self.assertEqual(True, plugin.loader.auto_reload)
+        self.assertEqual(25, plugin.loader._cache.capacity)
+
+    def test_init_with_loader_options(self):
+        plugin = TextTemplateEnginePlugin(options={
+            'genshi.auto_reload': 'off',
+            'genshi.max_cache_size': '100',
+            'genshi.search_path': '/usr/share/tmpl:/usr/local/share/tmpl',
+        })
+        self.assertEqual(['/usr/share/tmpl', '/usr/local/share/tmpl'],
+                         plugin.loader.search_path)
+        self.assertEqual(False, plugin.loader.auto_reload)
+        self.assertEqual(100, plugin.loader._cache.capacity)
+
+    def test_init_with_output_options(self):
+        plugin = TextTemplateEnginePlugin(options={
+            'genshi.default_encoding': 'iso-8859-15',
+        })
+        self.assertEqual('iso-8859-15', plugin.default_encoding)
+
+    def test_load_template_from_file(self):
+        plugin = TextTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.test')
+        assert isinstance(tmpl, TextTemplate)
+        self.assertEqual('test.txt', os.path.basename(tmpl.filename))
+
+    def test_load_template_from_string(self):
+        plugin = TextTemplateEnginePlugin()
+        tmpl = plugin.load_template(None, template_string="$message")
+        self.assertEqual(None, tmpl.filename)
+        assert isinstance(tmpl, TextTemplate)
+
+    def test_transform_without_load(self):
+        plugin = TextTemplateEnginePlugin()
+        stream = plugin.transform({'message': 'Hello'},
+                                  PACKAGE + '.templates.test')
+        assert isinstance(stream, Stream)
+
+    def test_transform_with_load(self):
+        plugin = TextTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.test')
+        stream = plugin.transform({'message': 'Hello'}, tmpl)
+        assert isinstance(stream, Stream)
+
+    def test_render(self):
+        plugin = TextTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.test')
+        output = plugin.render({'message': 'Hello'}, template=tmpl)
+        self.assertEqual("""Test
+====
+
+Hello
+""", output)
+
+    def test_helper_functions(self):
+        plugin = TextTemplateEnginePlugin()
+        tmpl = plugin.load_template(PACKAGE + '.templates.functions')
+        output = plugin.render({}, template=tmpl)
+        self.assertEqual("""False
+bar
+""", output)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(MarkupTemplateEnginePluginTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(TextTemplateEnginePluginTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/templates/functions.html
@@ -0,0 +1,6 @@
+<div>
+${defined('foo')}
+${value_of('foo', 'bar')}
+${XML(snippet)}
+${HTML(snippet)}
+</div>
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/templates/functions.txt
@@ -0,0 +1,2 @@
+${defined('foo')}
+${value_of('foo', 'bar')}
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/templates/test.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      lang="en">
+
+  <head>
+    <title>Test</title>
+  </head>
+
+  <body>
+    <h1>Test</h1>
+    <p>$message</p>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/templates/test.txt
@@ -0,0 +1,4 @@
+Test
+====
+
+$message
Copyright (C) 2012-2017 Edgewall Software