# 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 @@ $table
""") +genshi_text_tmpl = NewTextTemplate(""" + +{% for row in table %} +{% for c in row.values() %}{% end %} +{% end %} +
$c
+""") + 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 %}{% 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