# HG changeset patch
# User cmlenz
# Date 1187008856 0
# Node ID 1da8de3e5e51ae0b9d5d01f35e591e0d29b3a08f
# Parent 36b5a03534a0a0ded178c71972b2f9a7c07d0730
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.
diff --git a/ChangeLog b/ChangeLog
--- 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
diff --git a/UPGRADE.txt b/UPGRADE.txt
--- 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
------------------------------------
diff --git a/doc/plugin.txt b/doc/plugin.txt
--- 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
diff --git a/doc/templates.txt b/doc/templates.txt
--- a/doc/templates.txt
+++ b/doc/templates.txt
@@ -116,9 +116,12 @@
>>> from genshi.template import MarkupTemplate
>>> tmpl = MarkupTemplate('
Hello, $name!
')
>>> stream = tmpl.generate(name='world')
- >>> print stream.render()
+ >>> print stream.render('xhtml')
Hello, world!
+.. 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
diff --git a/doc/text-templates.txt b/doc/text-templates.txt
--- 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 `_ 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.
diff --git a/examples/bench/basic.py b/examples/bench/basic.py
--- 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)
diff --git a/examples/bench/bigtable.py b/examples/bench/bigtable.py
--- 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 @@
""")
+genshi_text_tmpl = NewTextTemplate("""
+
+{% for row in table %}
+{% for c in row.values() %}$c | {% end %}
+
{% end %}
+
+""")
+
if DjangoTemplate:
django_tmpl = DjangoTemplate("""
@@ -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)
diff --git a/examples/bench/genshi_text/footer.txt b/examples/bench/genshi_text/footer.txt
new file mode 100644
--- /dev/null
+++ b/examples/bench/genshi_text/footer.txt
@@ -0,0 +1,1 @@
+
diff --git a/examples/bench/genshi_text/header.txt b/examples/bench/genshi_text/header.txt
new file mode 100644
--- /dev/null
+++ b/examples/bench/genshi_text/header.txt
@@ -0,0 +1,3 @@
+
diff --git a/examples/bench/genshi_text/template.txt b/examples/bench/genshi_text/template.txt
new file mode 100644
--- /dev/null
+++ b/examples/bench/genshi_text/template.txt
@@ -0,0 +1,28 @@
+
+
+
+
+ ${escape(title)}
+
+
+
+ {% include header.txt %}
+ {% def greeting(name) %}
+ Hello, ${name}!
+ {% end %}
+
+ ${greeting(user)}
+ ${greeting("me")}
+ ${greeting("world")}
+
+ Loop
+ {% if items %}
+ {% for idx, item in enumerate(items) %}\
+ - ${escape(item)}
+ {% end %}
+
{% end %}
+
+ {% include footer.txt %}
+
diff --git a/genshi/template/__init__.py b/genshi/template/__init__.py
--- 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'
diff --git a/genshi/template/plugin.py b/genshi/template/plugin.py
--- 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)
diff --git a/genshi/template/tests/plugin.py b/genshi/template/tests/plugin.py
--- 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')
diff --git a/genshi/template/tests/templates/new_syntax.txt b/genshi/template/tests/templates/new_syntax.txt
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
diff --git a/genshi/template/tests/text.py b/genshi/template/tests/text.py
--- 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__':
diff --git a/genshi/template/text.py b/genshi/template/text.py
--- 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,
+
+
+ We have the following items for you:
+
+ * Item 1
+
+ * Item 2
+
+ * Item 3
+
+
+
+ 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,
+
+ We have the following items for you:
+ * 1
+ * 2
+ * 3
+
+
+ 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,
+
+ {# This is a comment #}
+ We have the following items for you:
+ * 1
+ * 2
+ * 3
+
+
+ :since: version 0.5
+ """
+ directives = [('def', DefDirective),
+ ('when', WhenDirective),
+ ('otherwise', OtherwiseDirective),
+ ('for', ForDirective),
+ ('if', IfDirective),
+ ('choose', ChooseDirective),
+ ('with', WithDirective)]
+
+ _DIRECTIVE_RE = r'((? 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