changeset 592:7145e4eba2ec

Add a new syntax for text templates, which is available alongside the old syntax for now. The new syntax is more poweful and flexible, using Django-style directive notation.
author cmlenz
date Mon, 13 Aug 2007 12:40:56 +0000
parents 880b1a75d046
children aa5762c7b7f1
files ChangeLog UPGRADE.txt doc/plugin.txt doc/templates.txt doc/text-templates.txt examples/bench/basic.py examples/bench/bigtable.py examples/bench/genshi_text/footer.txt examples/bench/genshi_text/header.txt examples/bench/genshi_text/template.txt genshi/template/__init__.py genshi/template/plugin.py genshi/template/tests/plugin.py genshi/template/tests/templates/new_syntax.txt genshi/template/tests/text.py genshi/template/text.py
diffstat 16 files changed, 602 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -17,6 +17,11 @@
    only if the template loader is not set to do automatic reloading. Included
    templates are basically inlined into the including template, which can
    speed up rendering of that template a bit.
+ * Added new syntax for text templates, which is more powerful and flexible
+   with respect to white-space and line breaks. The old syntax is still
+   available and the default for now, but in a future release the new syntax
+   will become the default, and some time affter that the old syntax will be
+   removed.
 
 
 Version 0.4.4
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -1,6 +1,20 @@
 Upgrading Genshi
 ================
 
+Upgrading from Genshi 0.4.x to 0.5.x
+------------------------------------
+
+Genshi 0.5 introduces a new, alternative syntax for text templates, which is
+more flexible and powerful compared to the old syntax. For backwards
+compatibility, this new syntax is not used by default, though it will be in
+a future version. It is recommended that you migrate to using this new syntax.
+To do so, simply rename any references in your code to `TextTemplate` to
+`NewTextTemplate`. To explicitly use the old syntax, use `OldTextTemplate`
+instead, so that you can be sure you'll be using the same language when the
+default in Genshi is changed (at least until the old implementation is
+completely removed).
+
+
 Upgrading from Genshi 0.3.x to 0.4.x
 ------------------------------------
 
--- a/doc/plugin.txt
+++ b/doc/plugin.txt
@@ -222,6 +222,14 @@
 The default value is **25**. You may want to choose a higher value if your web
 site uses a larger number of templates, and you have enough memory to spare.
 
+``genshi.new_text_syntax``
+--------------------------
+Whether the new syntax for text templates should be used. Specify "yes" to
+enable the new syntax, or "no" to use the old syntax.
+
+In the version of Genshi, the default is to use the old syntax for
+backwards-compatibility, but that will change in a future release.
+
 ``genshi.search_path``
 ----------------------
 A colon-separated list of file-system path names that the template loader should
--- a/doc/templates.txt
+++ b/doc/templates.txt
@@ -116,9 +116,12 @@
   >>> from genshi.template import MarkupTemplate
   >>> tmpl = MarkupTemplate('<h1>Hello, $name!</h1>')
   >>> stream = tmpl.generate(name='world')
-  >>> print stream.render()
+  >>> print stream.render('xhtml')
   <h1>Hello, world!</h1>
 
+.. note:: See the Serialization_ section of the `Markup Streams`_ page for
+          information on configuring template output options.
+
 Using a text template is similar:
 
 .. code-block:: pycon
@@ -126,13 +129,15 @@
   >>> from genshi.template import TextTemplate
   >>> tmpl = TextTemplate('Hello, $name!')
   >>> stream = tmpl.generate(name='world')
-  >>> print stream.render()
+  >>> print stream.render('text')
   Hello, world!
 
-.. note:: See the Serialization_ section of the `Markup Streams`_ page for
-          information on configuring template output options.
+.. note:: If you want to use text templates, you should consider using the
+          ``NewTextTemplate`` class instead of simply ``TextTemplate``. See
+          the `Text Template Language`_ page.
 
 .. _serialization: streams.html#serialization
+.. _`Text Template Language`: text-templates.html
 .. _`Markup Streams`: streams.html
 
 Using a template loader provides the advantage that “compiled” templates are
--- a/doc/text-templates.txt
+++ b/doc/text-templates.txt
@@ -6,10 +6,7 @@
 
 In addition to the XML-based template language, Genshi provides a simple
 text-based template language, intended for basic plain text generation needs.
-The language is similar to Cheetah_ or Velocity_.
-
-.. _cheetah: http://cheetahtemplate.org/
-.. _velocity: http://jakarta.apache.org/velocity/
+The language is similar to the Django_ template language.
 
 This document describes the template language and will be most useful as
 reference to those developing Genshi text templates. Templates are text files of
@@ -20,6 +17,13 @@
 See `Genshi Templating Basics <templates.html>`_ for general information on
 embedding Python code in templates.
 
+.. note:: Actually, Genshi currently has two different syntaxes for text
+          templates languages: One implemented by the class ``OldTextTemplate``
+          and another implemented by ``NewTextTemplate``. This documentation
+          concentrates on the latter, which is planned to completely replace the
+          older syntax. The older syntax is briefly described under legacy_.
+
+.. _django: http://www.djangoproject.com/
 
 .. contents:: Contents
    :depth: 3
@@ -32,38 +36,35 @@
 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
-ways: Genshi provides directives for conditionals and looping, among others.
+Directives are template commands enclosed by ``{% ... %}`` characters. 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
-first non-whitespace character on that line. Each directive must be “closed”
-using a ``#end`` marker. You can add after the ``#end`` marker, for example to
-document which directive is being closed, or even the expression associated with 
-that directive. Any text after ``#end`` (but on the same line) is  ignored, 
-and effectively treated as a comment.
+Each directive must be terminated using an ``{% end %}`` marker. You can add
+a string inside the ``{% end %}`` marker, for example to document which
+directive is being closed, or even the expression associated with  that
+directive. Any text after ``end`` inside the delimiters is  ignored,  and
+effectively treated as a comment.
 
-If you want to include a literal ``#`` in the output, you need to escape it
-by prepending a backslash character (``\``). Note that this is **not** required 
-if the ``#`` isn't immediately followed by a letter, or it isn't the first
-non-whitespace character on the line.
+If you want to include a literal delimiter in the output, you need to escape it
+by prepending a backslash character (``\``).
 
 
 Conditional Sections
 ====================
 
-.. _`#if`:
+.. _`if`:
 
-``#if``
----------
+``{% if %}``
+------------
 
 The content is only rendered if the expression evaluates to a truth value:
 
 .. code-block:: genshitext
 
-  #if foo
+  {% if foo %}
     ${bar}
-  #end
+  {% end %}
 
 Given the data ``foo=True`` and ``bar='Hello'`` in the template context, this
 would produce::
@@ -71,58 +72,46 @@
     Hello
 
 
-.. _`#choose`:
-.. _`#when`:
-.. _`#otherwise`:
-
-``#choose``
--------------
+.. _`choose`:
+.. _`when`:
+.. _`otherwise`:
 
-The ``#choose`` directive, in combination with the directives ``#when`` and
-``#otherwise`` provides advanced contional processing for rendering one of
-several alternatives. The first matching ``#when`` branch is rendered, or, if
-no ``#when`` branch matches, the ``#otherwise`` branch is be rendered.
+``{% choose %}``
+----------------
 
-If the ``#choose`` directive has no argument the nested ``#when`` directives
-will be tested for truth:
+The ``choose`` directive, in combination with the directives ``when`` and
+``otherwise``, provides advanced contional processing for rendering one of
+several alternatives. The first matching ``when`` branch is rendered, or, if
+no ``when`` branch matches, the ``otherwise`` branch is be rendered.
+
+If the ``choose`` directive has no argument the nested ``when`` directives will
+be tested for truth:
 
 .. code-block:: genshitext
 
   The answer is:
-  #choose
-    #when 0 == 1
-      0
-    #end
-    #when 1 == 1
-      1
-    #end
-    #otherwise
-      2
-    #end
-  #end
+  {% choose %}
+    {% when 0 == 1 %}0{% end %}
+    {% when 1 == 1 %}1{% end %}
+    {% otherwise %}2{% end %}
+  {% end %}
 
 This would produce the following output::
 
   The answer is:
-      1
+    1
 
-If the ``#choose`` does have an argument, the nested ``#when`` directives will
-be tested for equality to the parent ``#choose`` value:
+If the ``choose`` does have an argument, the nested ``when`` directives will
+be tested for equality to the parent ``choose`` value:
 
 .. code-block:: genshitext
 
   The answer is:
-  #choose 1
-    #when 0
-      0
-    #end
-    #when 1
-      1
-    #end
-    #otherwise
-      2
-    #end
-  #end
+  {% choose 1 %}\
+    {% when 0 %}0{% end %}\
+    {% when 1 %}1{% end %}\
+    {% otherwise %}2{% end %}\
+  {% end %}
 
 This would produce the following output::
 
@@ -133,19 +122,19 @@
 Looping
 =======
 
-.. _`#for`:
+.. _`for`:
 
-``#for``
-----------
+``{% for %}``
+-------------
 
 The content is repeated for every item in an iterable:
 
 .. code-block:: genshitext
 
   Your items:
-  #for item in items
+  {% for item in items %}\
     * ${item}
-  #end
+  {% end %}
 
 Given ``items=[1, 2, 3]`` in the context data, this would produce::
 
@@ -158,21 +147,21 @@
 Snippet Reuse
 =============
 
-.. _`#def`:
+.. _`def`:
 .. _`macros`:
 
-``#def``
-----------
+``{% def %}``
+-------------
 
-The ``#def`` directive can be used to create macros, i.e. snippets of template
+The ``def`` directive can be used to create macros, i.e. snippets of template
 text that have a name and optionally some parameters, and that can be inserted
 in other places:
 
 .. code-block:: genshitext
 
-  #def greeting(name)
+  {% def greeting(name) %}
     Hello, ${name}!
-  #end
+  {% end %}
   ${greeting('world')}
   ${greeting('everyone else')}
 
@@ -181,15 +170,15 @@
     Hello, world!
     Hello, everyone else!
 
-If a macro doesn't require parameters, it can be defined as well as called
-without the parenthesis. For example:
+If a macro doesn't require parameters, it can be defined without the
+parenthesis. For example:
 
 .. code-block:: genshitext
 
-  #def greeting
+  {% def greeting %}
     Hello, world!
-  #end
-  ${greeting}
+  {% end %}
+  ${greeting()}
 
 The above would be rendered to::
 
@@ -197,17 +186,17 @@
 
 
 .. _includes:
-.. _`#include`:
+.. _`include`:
 
-``#include``
-------------
+``{% include %}``
+-----------------
 
 To reuse common parts of template text across template files, you can include
-other files using the ``#include`` directive:
+other files using the ``include`` directive:
 
 .. code-block:: genshitext
 
-  #include "base.txt"
+  {% include base.txt %}
 
 Any content included this way is inserted into the generated output. The
 included template sees the context data as it exists at the point of the
@@ -220,13 +209,13 @@
 the included file at "``myapp/base.txt``". You can also use Unix-style
 relative paths, for example "``../base.txt``" to look in the parent directory.
 
-Just like other directives, the argument to the ``#include`` directive accepts
+Just like other directives, the argument to the ``include`` directive accepts
 any Python expression, so the path to the included template can be determined
 dynamically:
 
 .. code-block:: genshitext
 
-  #include '%s.txt' % filename
+  {% include '%s.txt' % filename %}
 
 Note that a ``TemplateNotFound`` exception is raised if an included file can't
 be found.
@@ -237,12 +226,12 @@
 Variable Binding
 ================
 
-.. _`#with`:
+.. _`with`:
 
-``#with``
------------
+``{% with %}``
+--------------
 
-The ``#with`` directive lets you assign expressions to variables, which can
+The ``{% with %}`` directive lets you assign expressions to variables, which can
 be used to make expressions inside the directive less verbose and more
 efficient. For example, if you need use the expression ``author.posts`` more
 than once, and that actually results in a database query, assigning the results
@@ -253,9 +242,9 @@
 .. code-block:: genshitext
 
   Magic numbers!
-  #with y=7; z=x+10
+  {% with y=7; z=x+10 %}
     $x $y $z
-  #end
+  {% end %}
 
 Given ``x=42`` in the context data, this would produce::
 
@@ -263,17 +252,107 @@
     42 7 52
 
 Note that if a variable of the same name already existed outside of the scope
-of the ``#with`` directive, it will **not** be overwritten. Instead, it will
-have the same value it had prior to the ``#with`` assignment. Effectively,
+of the ``with`` directive, it will **not** be overwritten. Instead, it will
+have the same value it had prior to the ``with`` assignment. Effectively,
 this means that variables are immutable in Genshi.
 
 
+.. _whitespace:
+
+---------------------------
+White-space and Line Breaks
+---------------------------
+
+Note that space or line breaks around directives is never automatically removed.
+Consider the following example:
+
+.. code-block:: genshitext
+
+  {% for item in items %}
+    {% if item.visible %}
+      ${item}
+    {% end %}
+  {% end %}
+
+This will result in two empty lines above and beneath every item, plus the
+spaces used for indentation. If you want to supress a line break, simply end
+the line with a backslash:
+
+.. code-block:: genshitext
+
+  {% for item in items %}\
+    {% if item.visible %}\
+      ${item}
+    {% end %}\
+  {% end %}\
+
+Now there would be no empty lines between the items in the output. But you still
+get the spaces used for indentation, and because the line breaks are removed,
+they actually continue and add up between lines. There are numerous ways to
+control white-space in the output while keeping the template readable, such as
+moving the indentation into the delimiters, or moving the end delimiter on the
+next line, and so on.
+
+
 .. _comments:
 
 --------
 Comments
 --------
 
-Lines where the first non-whitespace characters are ``##`` are removed from
-the output, and can thus be used for comments. This can be escaped using a
+Parts in templates can be commented out using the delimiters ``{# ... #}``.
+Any content in comments are removed from the output.
+
+.. code-block:: genshitext
+
+  {# This won't end up in the output #}
+  This will.
+
+Just like directive delimiters, these can be escaped by prefixing with a
 backslash.
+
+.. code-block:: genshitext
+
+  \{# This *will* end up in the output, including delimiters #}
+  This too.
+
+
+.. _legacy:
+
+---------------------------
+Legacy Text Template Syntax
+---------------------------
+
+The syntax for text templates was redesigned in version 0.5 of Genshi to make
+the language more flexible and powerful. The older syntax is based on line
+starting with dollar signs, similar to e.g. Cheetah_ or Velocity_.
+
+.. _cheetah: http://cheetahtemplate.org/
+.. _velocity: http://jakarta.apache.org/velocity/
+
+A simple template using the old syntax looked like this:
+
+.. code-block:: genshitext
+
+  Dear $name,
+  
+  We have the following items for you:
+  #for item in items
+   * $item
+  #end
+  
+  All the best,
+  Foobar
+
+Beyond the requirement of putting directives on separate lines prefixed with
+dollar signs, the language itself is very similar to the new one. Except that
+comments are lines that start with two ``#`` characters, and a line-break at the
+end of a directive is removed automatically.
+
+.. note:: If you're using this old syntax, it is strongly recommended to migrate
+          to the new syntax. Simply replace any references to ``TextTemplate``
+          by ``NewTextTemplate``. On the other hand, if you want to stick with
+          the old syntax for a while longer, replace references to
+          ``TextTemplate`` by ``OldTextTemplate``; while ``TextTemplate`` is
+          still an alias for the old language at this point, that will change
+          in a future release.
--- a/examples/bench/basic.py
+++ b/examples/bench/basic.py
@@ -9,7 +9,8 @@
 import sys
 import timeit
 
-__all__ = ['clearsilver', 'mako', 'django', 'kid', 'genshi', 'simpletal']
+__all__ = ['clearsilver', 'mako', 'django', 'kid', 'genshi', 'genshi_text',
+           'simpletal']
 
 def genshi(dirname, verbose=False):
     from genshi.template import TemplateLoader
@@ -24,6 +25,20 @@
         print render()
     return render
 
+def genshi_text(dirname, verbose=False):
+    from genshi.core import escape
+    from genshi.template import TemplateLoader, NewTextTemplate
+    loader = TemplateLoader([dirname], auto_reload=False)
+    template = loader.load('template.txt', cls=NewTextTemplate)
+    def render():
+        data = dict(escape=escape, title='Just a test', user='joe',
+                    items=['Number %d' % num for num in range(1, 15)])
+        return template.generate(**data).render('text')
+
+    if verbose:
+        print render()
+    return render
+
 def mako(dirname, verbose=False):
     from mako.lookup import TemplateLookup
     lookup = TemplateLookup(directories=[dirname], filesystem_checks=False)
--- a/examples/bench/bigtable.py
+++ b/examples/bench/bigtable.py
@@ -10,7 +10,7 @@
 import timeit
 from StringIO import StringIO
 from genshi.builder import tag
-from genshi.template import MarkupTemplate
+from genshi.template import MarkupTemplate, NewTextTemplate
 
 try:
     from elementtree import ElementTree as et
@@ -60,6 +60,14 @@
 <table xmlns:py="http://genshi.edgewall.org/">$table</table>
 """)
 
+genshi_text_tmpl = NewTextTemplate("""
+<table>
+{% for row in table %}<tr>
+{% for c in row.values() %}<td>$c</td>{% end %}
+</tr>{% end %}
+</table>
+""")
+
 if DjangoTemplate:
     django_tmpl = DjangoTemplate("""
     <table>
@@ -95,6 +103,11 @@
     stream = genshi_tmpl.generate(table=table)
     stream.render('html', strip_whitespace=False)
 
+def test_genshi_text():
+    """Genshi text template"""
+    stream = genshi_text_tmpl.generate(table=table)
+    stream.render('text')
+
 def test_genshi_builder():
     """Genshi template + tag builder"""
     stream = tag.TABLE([
@@ -183,9 +196,9 @@
 
 
 def run(which=None, number=10):
-    tests = ['test_builder', 'test_genshi', 'test_genshi_builder',
-             'test_mako', 'test_kid', 'test_kid_et', 'test_et', 'test_cet',
-             'test_clearsilver', 'test_django']
+    tests = ['test_builder', 'test_genshi', 'test_genshi_text',
+             'test_genshi_builder', 'test_mako', 'test_kid', 'test_kid_et',
+             'test_et', 'test_cet', 'test_clearsilver', 'test_django']
 
     if which:
         tests = filter(lambda n: n[5:] in which, tests)
new file mode 100644
--- /dev/null
+++ b/examples/bench/genshi_text/footer.txt
@@ -0,0 +1,1 @@
+<div id="footer"></div>
new file mode 100644
--- /dev/null
+++ b/examples/bench/genshi_text/header.txt
@@ -0,0 +1,3 @@
+<div id="header">
+  <h1>${escape(title)}</h1>
+</div>
new file mode 100644
--- /dev/null
+++ b/examples/bench/genshi_text/template.txt
@@ -0,0 +1,28 @@
+<!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>${escape(title)}</title>
+</head>
+
+<body>
+  {% include header.txt %}
+  {% def greeting(name) %}
+    Hello, ${name}!
+  {% end %}
+
+  <div>${greeting(user)}</div>
+  <div>${greeting("me")}</div>
+  <div>${greeting("world")}</div>
+  
+  <h2>Loop</h2>
+  {% if items %}<ul>
+    {% for idx, item in enumerate(items) %}\
+      <li{% if idx + 1 == len(items) %} class="last"{% end %}>${escape(item)}</li>
+    {% end %}
+  </ul>{% end %}
+  
+  {% include footer.txt %}
+</body>
--- a/genshi/template/__init__.py
+++ b/genshi/template/__init__.py
@@ -18,6 +18,6 @@
                                  BadDirectiveError
 from genshi.template.loader import TemplateLoader, TemplateNotFound
 from genshi.template.markup import MarkupTemplate
-from genshi.template.text import TextTemplate
+from genshi.template.text import TextTemplate, OldTextTemplate, NewTextTemplate
 
 __docformat__ = 'restructuredtext en'
--- a/genshi/template/plugin.py
+++ b/genshi/template/plugin.py
@@ -23,7 +23,7 @@
 from genshi.template.base import Template
 from genshi.template.loader import TemplateLoader
 from genshi.template.markup import MarkupTemplate
-from genshi.template.text import TextTemplate
+from genshi.template.text import TextTemplate, NewTextTemplate
 
 __all__ = ['ConfigurationError', 'AbstractTemplateEnginePlugin',
            'MarkupTemplateEnginePlugin', 'TextTemplateEnginePlugin']
@@ -162,3 +162,15 @@
     template_class = TextTemplate
     extension = '.txt'
     default_format = 'text'
+
+    def __init__(self, extra_vars_func=None, options=None):
+        if options is None:
+            options = {}
+
+        new_syntax = options.get('genshi.new_text_syntax')
+        if isinstance(new_syntax, basestring):
+            new_syntax = new_syntax.lower() in ('1', 'on', 'yes', 'true')
+        if new_syntax:
+            self.template_class = NewTextTemplate
+
+        AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
--- a/genshi/template/tests/plugin.py
+++ b/genshi/template/tests/plugin.py
@@ -18,7 +18,7 @@
 
 from genshi.core import Stream
 from genshi.output import DocType
-from genshi.template import MarkupTemplate, TextTemplate
+from genshi.template import MarkupTemplate, TextTemplate, NewTextTemplate
 from genshi.template.plugin import ConfigurationError, \
                                    MarkupTemplateEnginePlugin, \
                                    TextTemplateEnginePlugin
@@ -185,6 +185,15 @@
         })
         self.assertEqual('iso-8859-15', plugin.default_encoding)
 
+    def test_init_with_new_syntax(self):
+        plugin = TextTemplateEnginePlugin(options={
+            'genshi.new_text_syntax': 'yes',
+        })
+        self.assertEqual(NewTextTemplate, plugin.template_class)
+        tmpl = plugin.load_template(PACKAGE + '.templates.new_syntax')
+        output = plugin.render({'foo': True}, template=tmpl)
+        self.assertEqual('bar', output)
+
     def test_load_template_from_file(self):
         plugin = TextTemplateEnginePlugin()
         tmpl = plugin.load_template(PACKAGE + '.templates.test')
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/templates/new_syntax.txt
@@ -0,0 +1,1 @@
+{% if foo %}bar{% end %}
\ No newline at end of file
--- a/genshi/template/tests/text.py
+++ b/genshi/template/tests/text.py
@@ -18,10 +18,10 @@
 import unittest
 
 from genshi.template.loader import TemplateLoader
-from genshi.template.text import TextTemplate
+from genshi.template.text import OldTextTemplate, NewTextTemplate
 
 
-class TextTemplateTestCase(unittest.TestCase):
+class OldTextTemplateTestCase(unittest.TestCase):
     """Tests for text template processing."""
 
     def setUp(self):
@@ -31,19 +31,19 @@
         shutil.rmtree(self.dirname)
 
     def test_escaping(self):
-        tmpl = TextTemplate('\\#escaped')
+        tmpl = OldTextTemplate('\\#escaped')
         self.assertEqual('#escaped', str(tmpl.generate()))
 
     def test_comment(self):
-        tmpl = TextTemplate('## a comment')
+        tmpl = OldTextTemplate('## a comment')
         self.assertEqual('', str(tmpl.generate()))
 
     def test_comment_escaping(self):
-        tmpl = TextTemplate('\\## escaped comment')
+        tmpl = OldTextTemplate('\\## escaped comment')
         self.assertEqual('## escaped comment', str(tmpl.generate()))
 
     def test_end_with_args(self):
-        tmpl = TextTemplate("""
+        tmpl = OldTextTemplate("""
         #if foo
           bar
         #end 'if foo'""")
@@ -51,16 +51,16 @@
 
     def test_latin1_encoded(self):
         text = u'$foo\xf6$bar'.encode('iso-8859-1')
-        tmpl = TextTemplate(text, encoding='iso-8859-1')
+        tmpl = OldTextTemplate(text, encoding='iso-8859-1')
         self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
 
     def test_unicode_input(self):
         text = u'$foo\xf6$bar'
-        tmpl = TextTemplate(text)
+        tmpl = OldTextTemplate(text)
         self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
 
     def test_empty_lines1(self):
-        tmpl = TextTemplate("""Your items:
+        tmpl = OldTextTemplate("""Your items:
 
         #for item in items
           * ${item}
@@ -73,7 +73,7 @@
 """, tmpl.generate(items=range(3)).render('text'))
 
     def test_empty_lines2(self):
-        tmpl = TextTemplate("""Your items:
+        tmpl = OldTextTemplate("""Your items:
 
         #for item in items
           * ${item}
@@ -105,16 +105,108 @@
             file2.close()
 
         loader = TemplateLoader([self.dirname])
-        tmpl = loader.load('tmpl2.txt', cls=TextTemplate)
+        tmpl = loader.load('tmpl2.txt', cls=OldTextTemplate)
         self.assertEqual("""----- Included data below this line -----
 Included
             ----- Included data above this line -----""",
                          tmpl.generate().render())
-        
+
+
+class NewTextTemplateTestCase(unittest.TestCase):
+    """Tests for text template processing."""
+
+    def setUp(self):
+        self.dirname = tempfile.mkdtemp(suffix='markup_test')
+
+    def tearDown(self):
+        shutil.rmtree(self.dirname)
+
+    def test_escaping(self):
+        tmpl = NewTextTemplate('\\{% escaped %}')
+        self.assertEqual('{% escaped %}', str(tmpl.generate()))
+
+    def test_comment(self):
+        tmpl = NewTextTemplate('{# a comment #}')
+        self.assertEqual('', str(tmpl.generate()))
+
+    def test_comment_escaping(self):
+        tmpl = NewTextTemplate('\\{# escaped comment #}')
+        self.assertEqual('{# escaped comment #}', str(tmpl.generate()))
+
+    def test_end_with_args(self):
+        tmpl = NewTextTemplate("""
+{% if foo %}
+  bar
+{% end 'if foo' %}""")
+        self.assertEqual('\n', str(tmpl.generate(foo=False)))
+
+    def test_latin1_encoded(self):
+        text = u'$foo\xf6$bar'.encode('iso-8859-1')
+        tmpl = NewTextTemplate(text, encoding='iso-8859-1')
+        self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
+
+    def test_unicode_input(self):
+        text = u'$foo\xf6$bar'
+        tmpl = NewTextTemplate(text)
+        self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
+
+    def test_empty_lines1(self):
+        tmpl = NewTextTemplate("""Your items:
+
+{% for item in items %}\
+  * ${item}
+{% end %}""")
+        self.assertEqual("""Your items:
+
+  * 0
+  * 1
+  * 2
+""", tmpl.generate(items=range(3)).render('text'))
+
+    def test_empty_lines2(self):
+        tmpl = NewTextTemplate("""Your items:
+
+{% for item in items %}\
+  * ${item}
+
+{% end %}""")
+        self.assertEqual("""Your items:
+
+  * 0
+
+  * 1
+
+  * 2
+
+""", tmpl.generate(items=range(3)).render('text'))
+
+    def test_include(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w')
+        try:
+            file1.write("Included\n")
+        finally:
+            file1.close()
+
+        file2 = open(os.path.join(self.dirname, 'tmpl2.txt'), 'w')
+        try:
+            file2.write("""----- Included data below this line -----
+{% include tmpl1.txt %}
+----- Included data above this line -----""")
+        finally:
+            file2.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl = loader.load('tmpl2.txt', cls=NewTextTemplate)
+        self.assertEqual("""----- Included data below this line -----
+Included
+----- Included data above this line -----""", tmpl.generate().render())
+
+
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(TextTemplate.__module__))
-    suite.addTest(unittest.makeSuite(TextTemplateTestCase, 'test'))
+    suite.addTest(doctest.DocTestSuite(NewTextTemplate.__module__))
+    suite.addTest(unittest.makeSuite(OldTextTemplateTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(NewTextTemplateTestCase, 'test'))
     return suite
 
 if __name__ == '__main__':
--- a/genshi/template/text.py
+++ b/genshi/template/text.py
@@ -11,7 +11,20 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://genshi.edgewall.org/log/.
 
-"""Plain text templating engine."""
+"""Plain text templating engine.
+
+This module implements two template language syntaxes, at least for a certain
+transitional period. `OldTextTemplate` (aliased to just `TextTemplate`) defines
+a syntax that was inspired by Cheetah/Velocity. `NewTextTemplate` on the other
+hand is inspired by the syntax of the Django template language, which has more
+explicit delimiting of directives, and is more flexible with regards to
+white space and line breaks.
+
+In a future release, `OldTextTemplate` will be phased out in favor of
+`NewTextTemplate`, as the names imply. Therefore the new syntax is strongly
+recommended for new projects, and existing projects may want to migrate to the
+new syntax to remain compatible with future Genshi releases.
+"""
 
 import re
 
@@ -20,14 +33,192 @@
 from genshi.template.directives import Directive, _apply_directives
 from genshi.template.interpolation import interpolate
 
-__all__ = ['TextTemplate']
+__all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate']
 __docformat__ = 'restructuredtext en'
 
 
-class TextTemplate(Template):
-    """Implementation of a simple text-based template engine.
+class NewTextTemplate(Template):
+    r"""Implementation of a simple text-based template engine. This class will
+    replace `OldTextTemplate` in a future release.
     
-    >>> tmpl = TextTemplate('''Dear $name,
+    It uses a more explicit delimiting style for directives: instead of the old
+    style which required putting directives on separate lines that were prefixed
+    with a ``#`` sign, directives and commenbtsr are enclosed in delimiter pairs
+    (by default ``{% ... %}`` and ``{# ... #}``, respectively).
+    
+    Variable substitution uses the same interpolation syntax as for markup
+    languages: simple references are prefixed with a dollar sign, more complex
+    expression enclosed in curly braces.
+    
+    >>> tmpl = NewTextTemplate('''Dear $name,
+    ... 
+    ... {# This is a comment #}
+    ... We have the following items for you:
+    ... {% for item in items %}
+    ...  * ${'Item %d' % item}
+    ... {% end %}
+    ... ''')
+    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
+    Dear Joe,
+    <BLANKLINE>
+    <BLANKLINE>
+    We have the following items for you:
+    <BLANKLINE>
+     * Item 1
+    <BLANKLINE>
+     * Item 2
+    <BLANKLINE>
+     * Item 3
+    <BLANKLINE>
+    <BLANKLINE>
+    
+    By default, no spaces or line breaks are removed. If a line break should
+    not be included in the output, prefix it with a backslash:
+    
+    >>> tmpl = NewTextTemplate('''Dear $name,
+    ... 
+    ... {# This is a comment #}\
+    ... We have the following items for you:
+    ... {% for item in items %}\
+    ...  * $item
+    ... {% end %}\
+    ... ''')
+    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
+    Dear Joe,
+    <BLANKLINE>
+    We have the following items for you:
+     * 1
+     * 2
+     * 3
+    <BLANKLINE>
+    
+    Backslashes are also used to escape the start delimiter of directives and
+    comments:
+
+    >>> tmpl = NewTextTemplate('''Dear $name,
+    ... 
+    ... \{# This is a comment #}
+    ... We have the following items for you:
+    ... {% for item in items %}\
+    ...  * $item
+    ... {% end %}\
+    ... ''')
+    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
+    Dear Joe,
+    <BLANKLINE>
+    {# This is a comment #}
+    We have the following items for you:
+     * 1
+     * 2
+     * 3
+    <BLANKLINE>
+    
+    :since: version 0.5
+    """
+    directives = [('def', DefDirective),
+                  ('when', WhenDirective),
+                  ('otherwise', OtherwiseDirective),
+                  ('for', ForDirective),
+                  ('if', IfDirective),
+                  ('choose', ChooseDirective),
+                  ('with', WithDirective)]
+
+    _DIRECTIVE_RE = r'((?<!\\)%s\s*(\w+)\s*(.*?)\s*%s|(?<!\\)%s.*?%s)'
+    _ESCAPE_RE = r'\\\n|\\(\\)|\\(%s)|\\(%s)'
+
+    def __init__(self, source, basedir=None, filename=None, loader=None,
+                 encoding=None, lookup='lenient', allow_exec=False,
+                 delims=('{%', '%}', '{#', '#}')):
+        self.delimiters = delims
+        Template.__init__(self, source, basedir=basedir, filename=filename,
+                          loader=loader, encoding=encoding, lookup=lookup)
+
+    def _get_delims(self):
+        return self._delims
+    def _set_delims(self, delims):
+        if len(delims) != 4:
+            raise ValueError('delimiers tuple must have exactly four elements')
+        self._delims = delims
+        self._directive_re = re.compile(self._DIRECTIVE_RE % tuple(
+            map(re.escape, delims)
+        ))
+        self._escape_re = re.compile(self._ESCAPE_RE % tuple(
+            map(re.escape, delims[::2])
+        ))
+    delimiters = property(_get_delims, _set_delims, """\
+    The delimiters for directives and comments. This should be a four item tuple
+    of the form ``(directive_start, directive_end, comment_start,
+    comment_end)``, where each item is a string.
+    """)
+
+    def _parse(self, source, encoding):
+        """Parse the template from text input."""
+        stream = [] # list of events of the "compiled" template
+        dirmap = {} # temporary mapping of directives to elements
+        depth = 0
+
+        source = source.read()
+        if isinstance(source, str):
+            source = source.decode(encoding or 'utf-8', 'replace')
+        offset = 0
+        lineno = 1
+
+        _escape_sub = self._escape_re.sub
+        def _escape_repl(mo):
+            groups = filter(None, mo.groups()) 
+            if not groups:
+                return ''
+            return groups[0]
+
+        for idx, mo in enumerate(self._directive_re.finditer(source)):
+            start, end = mo.span(1)
+            if start > offset:
+                text = _escape_sub(_escape_repl, source[offset:start])
+                for kind, data, pos in interpolate(text, self.basedir,
+                                                   self.filename, lineno,
+                                                   lookup=self.lookup):
+                    stream.append((kind, data, pos))
+                lineno += len(text.splitlines())
+
+            lineno += len(source[start:end].splitlines())
+            command, value = mo.group(2, 3)
+            if command:
+                if command == 'include':
+                    pos = (self.filename, lineno, 0)
+                    stream.append((INCLUDE, (value.strip(), []), pos))
+                elif command == 'end':
+                    depth -= 1
+                    if depth in dirmap:
+                        directive, start_offset = dirmap.pop(depth)
+                        substream = stream[start_offset:]
+                        stream[start_offset:] = [(SUB, ([directive], substream),
+                                                  (self.filepath, lineno, 0))]
+                else:
+                    cls = self._dir_by_name.get(command)
+                    if cls is None:
+                        raise BadDirectiveError(command)
+                    directive = cls, value, None, (self.filepath, lineno, 0)
+                    dirmap[depth] = (directive, len(stream))
+                    depth += 1
+
+            offset = end
+
+        if offset < len(source):
+            text = _escape_sub(_escape_repl, source[offset:])
+            for kind, data, pos in interpolate(text, self.basedir,
+                                               self.filename, lineno,
+                                               lookup=self.lookup):
+                stream.append((kind, data, pos))
+
+        return stream
+
+
+class OldTextTemplate(Template):
+    """Legacy implementation of the old syntax text-based templates. This class
+    is provided in a transition phase for backwards compatibility. New code
+    should use the `NewTextTemplate` class and the improved syntax it provides.
+    
+    >>> tmpl = OldTextTemplate('''Dear $name,
     ... 
     ... We have the following items for you:
     ... #for item in items
@@ -117,3 +308,6 @@
                 stream.append((kind, data, pos))
 
         return stream
+
+
+TextTemplate = OldTextTemplate
Copyright (C) 2012-2017 Edgewall Software