Edgewall Software

Changeset 706


Ignore:
Timestamp:
Aug 13, 2007, 2:40:56 PM (16 years ago)
Author:
cmlenz
Message:

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.

Location:
trunk
Files:
5 added
12 edited

Legend:

Unmodified
Added
Removed
  • trunk/ChangeLog

    r704 r706  
    1818   templates are basically inlined into the including template, which can
    1919   speed up rendering of that template a bit.
     20 * Added new syntax for text templates, which is more powerful and flexible
     21   with respect to white-space and line breaks. The old syntax is still
     22   available and the default for now, but in a future release the new syntax
     23   will become the default, and some time affter that the old syntax will be
     24   removed.
    2025
    2126
  • trunk/UPGRADE.txt

    r546 r706  
    11Upgrading Genshi
    22================
     3
     4Upgrading from Genshi 0.4.x to 0.5.x
     5------------------------------------
     6
     7Genshi 0.5 introduces a new, alternative syntax for text templates, which is
     8more flexible and powerful compared to the old syntax. For backwards
     9compatibility, this new syntax is not used by default, though it will be in
     10a future version. It is recommended that you migrate to using this new syntax.
     11To do so, simply rename any references in your code to `TextTemplate` to
     12`NewTextTemplate`. To explicitly use the old syntax, use `OldTextTemplate`
     13instead, so that you can be sure you'll be using the same language when the
     14default in Genshi is changed (at least until the old implementation is
     15completely removed).
     16
    317
    418Upgrading from Genshi 0.3.x to 0.4.x
  • trunk/doc/plugin.txt

    r657 r706  
    223223site uses a larger number of templates, and you have enough memory to spare.
    224224
     225``genshi.new_text_syntax``
     226--------------------------
     227Whether the new syntax for text templates should be used. Specify "yes" to
     228enable the new syntax, or "no" to use the old syntax.
     229
     230In the version of Genshi, the default is to use the old syntax for
     231backwards-compatibility, but that will change in a future release.
     232
    225233``genshi.search_path``
    226234----------------------
  • trunk/doc/templates.txt

    r654 r706  
    117117  >>> tmpl = MarkupTemplate('<h1>Hello, $name!</h1>')
    118118  >>> stream = tmpl.generate(name='world')
    119   >>> print stream.render()
     119  >>> print stream.render('xhtml')
    120120  <h1>Hello, world!</h1>
     121
     122.. note:: See the Serialization_ section of the `Markup Streams`_ page for
     123          information on configuring template output options.
    121124
    122125Using a text template is similar:
     
    127130  >>> tmpl = TextTemplate('Hello, $name!')
    128131  >>> stream = tmpl.generate(name='world')
    129   >>> print stream.render()
     132  >>> print stream.render('text')
    130133  Hello, world!
    131134
    132 .. note:: See the Serialization_ section of the `Markup Streams`_ page for
    133           information on configuring template output options.
     135.. note:: If you want to use text templates, you should consider using the
     136          ``NewTextTemplate`` class instead of simply ``TextTemplate``. See
     137          the `Text Template Language`_ page.
    134138
    135139.. _serialization: streams.html#serialization
     140.. _`Text Template Language`: text-templates.html
    136141.. _`Markup Streams`: streams.html
    137142
  • trunk/doc/text-templates.txt

    r614 r706  
    77In addition to the XML-based template language, Genshi provides a simple
    88text-based template language, intended for basic plain text generation needs.
    9 The language is similar to Cheetah_ or Velocity_.
    10 
    11 .. _cheetah: http://cheetahtemplate.org/
    12 .. _velocity: http://jakarta.apache.org/velocity/
     9The language is similar to the Django_ template language.
    1310
    1411This document describes the template language and will be most useful as
     
    2118embedding Python code in templates.
    2219
     20.. note:: Actually, Genshi currently has two different syntaxes for text
     21          templates languages: One implemented by the class ``OldTextTemplate``
     22          and another implemented by ``NewTextTemplate``. This documentation
     23          concentrates on the latter, which is planned to completely replace the
     24          older syntax. The older syntax is briefly described under legacy_.
     25
     26.. _django: http://www.djangoproject.com/
    2327
    2428.. contents:: Contents
     
    3337-------------------
    3438
    35 Directives are lines starting with a ``#`` character followed immediately by
    36 the directive name. They can affect how the template is rendered in a number of
    37 ways: Genshi provides directives for conditionals and looping, among others.
    38 
    39 Directives must be on separate lines, and the ``#`` character must be be the
    40 first non-whitespace character on that line. Each directive must be “closed”
    41 using a ``#end`` marker. You can add after the ``#end`` marker, for example to
    42 document which directive is being closed, or even the expression associated with
    43 that directive. Any text after ``#end`` (but on the same line) is  ignored,
    44 and effectively treated as a comment.
    45 
    46 If you want to include a literal ``#`` in the output, you need to escape it
    47 by prepending a backslash character (``\``). Note that this is **not** required
    48 if the ``#`` isn't immediately followed by a letter, or it isn't the first
    49 non-whitespace character on the line.
     39Directives are template commands enclosed by ``{% ... %}`` characters. They can
     40affect how the template is rendered in a number of ways: Genshi provides
     41directives for conditionals and looping, among others.
     42
     43Each directive must be terminated using an ``{% end %}`` marker. You can add
     44a string inside the ``{% end %}`` marker, for example to document which
     45directive is being closed, or even the expression associated with  that
     46directive. Any text after ``end`` inside the delimiters is  ignored,  and
     47effectively treated as a comment.
     48
     49If you want to include a literal delimiter in the output, you need to escape it
     50by prepending a backslash character (``\``).
    5051
    5152
     
    5354====================
    5455
    55 .. _`#if`:
    56 
    57 ``#if``
    58 ---------
     56.. _`if`:
     57
     58``{% if %}``
     59------------
    5960
    6061The content is only rendered if the expression evaluates to a truth value:
     
    6263.. code-block:: genshitext
    6364
    64   #if foo
     65  {% if foo %}
    6566    ${bar}
    66   #end
     67  {% end %}
    6768
    6869Given the data ``foo=True`` and ``bar='Hello'`` in the template context, this
     
    7273
    7374
    74 .. _`#choose`:
    75 .. _`#when`:
    76 .. _`#otherwise`:
    77 
    78 ``#choose``
    79 -------------
    80 
    81 The ``#choose`` directive, in combination with the directives ``#when`` and
    82 ``#otherwise`` provides advanced contional processing for rendering one of
    83 several alternatives. The first matching ``#when`` branch is rendered, or, if
    84 no ``#when`` branch matches, the ``#otherwise`` branch is be rendered.
    85 
    86 If the ``#choose`` directive has no argument the nested ``#when`` directives
    87 will be tested for truth:
     75.. _`choose`:
     76.. _`when`:
     77.. _`otherwise`:
     78
     79``{% choose %}``
     80----------------
     81
     82The ``choose`` directive, in combination with the directives ``when`` and
     83``otherwise``, provides advanced contional processing for rendering one of
     84several alternatives. The first matching ``when`` branch is rendered, or, if
     85no ``when`` branch matches, the ``otherwise`` branch is be rendered.
     86
     87If the ``choose`` directive has no argument the nested ``when`` directives will
     88be tested for truth:
    8889
    8990.. code-block:: genshitext
    9091
    9192  The answer is:
    92   #choose
    93     #when 0 == 1
    94       0
    95     #end
    96     #when 1 == 1
    97       1
    98     #end
    99     #otherwise
    100       2
    101     #end
    102   #end
     93  {% choose %}
     94    {% when 0 == 1 %}0{% end %}
     95    {% when 1 == 1 %}1{% end %}
     96    {% otherwise %}2{% end %}
     97  {% end %}
     98
     99This would produce the following output::
     100
     101  The answer is:
     102    1
     103
     104If the ``choose`` does have an argument, the nested ``when`` directives will
     105be tested for equality to the parent ``choose`` value:
     106
     107.. code-block:: genshitext
     108
     109  The answer is:
     110  {% choose 1 %}\
     111    {% when 0 %}0{% end %}\
     112    {% when 1 %}1{% end %}\
     113    {% otherwise %}2{% end %}\
     114  {% end %}
    103115
    104116This would produce the following output::
     
    107119      1
    108120
    109 If the ``#choose`` does have an argument, the nested ``#when`` directives will
    110 be tested for equality to the parent ``#choose`` value:
    111 
    112 .. code-block:: genshitext
    113 
    114   The answer is:
    115   #choose 1
    116     #when 0
    117       0
    118     #end
    119     #when 1
    120       1
    121     #end
    122     #otherwise
    123       2
    124     #end
    125   #end
    126 
    127 This would produce the following output::
    128 
    129   The answer is:
    130       1
    131 
    132121
    133122Looping
    134123=======
    135124
    136 .. _`#for`:
    137 
    138 ``#for``
    139 ----------
     125.. _`for`:
     126
     127``{% for %}``
     128-------------
    140129
    141130The content is repeated for every item in an iterable:
     
    144133
    145134  Your items:
    146   #for item in items
     135  {% for item in items %}\
    147136    * ${item}
    148   #end
     137  {% end %}
    149138
    150139Given ``items=[1, 2, 3]`` in the context data, this would produce::
     
    159148=============
    160149
    161 .. _`#def`:
     150.. _`def`:
    162151.. _`macros`:
    163152
    164 ``#def``
    165 ----------
    166 
    167 The ``#def`` directive can be used to create macros, i.e. snippets of template
     153``{% def %}``
     154-------------
     155
     156The ``def`` directive can be used to create macros, i.e. snippets of template
    168157text that have a name and optionally some parameters, and that can be inserted
    169158in other places:
     
    171160.. code-block:: genshitext
    172161
    173   #def greeting(name)
     162  {% def greeting(name) %}
    174163    Hello, ${name}!
    175   #end
     164  {% end %}
    176165  ${greeting('world')}
    177166  ${greeting('everyone else')}
     
    182171    Hello, everyone else!
    183172
    184 If a macro doesn't require parameters, it can be defined as well as called
    185 without the parenthesis. For example:
    186 
    187 .. code-block:: genshitext
    188 
    189   #def greeting
     173If a macro doesn't require parameters, it can be defined without the
     174parenthesis. For example:
     175
     176.. code-block:: genshitext
     177
     178  {% def greeting %}
    190179    Hello, world!
    191   #end
    192   ${greeting}
     180  {% end %}
     181  ${greeting()}
    193182
    194183The above would be rendered to::
     
    198187
    199188.. _includes:
    200 .. _`#include`:
    201 
    202 ``#include``
    203 ------------
     189.. _`include`:
     190
     191``{% include %}``
     192-----------------
    204193
    205194To reuse common parts of template text across template files, you can include
    206 other files using the ``#include`` directive:
    207 
    208 .. code-block:: genshitext
    209 
    210   #include "base.txt"
     195other files using the ``include`` directive:
     196
     197.. code-block:: genshitext
     198
     199  {% include base.txt %}
    211200
    212201Any content included this way is inserted into the generated output. The
     
    221210relative paths, for example "``../base.txt``" to look in the parent directory.
    222211
    223 Just like other directives, the argument to the ``#include`` directive accepts
     212Just like other directives, the argument to the ``include`` directive accepts
    224213any Python expression, so the path to the included template can be determined
    225214dynamically:
     
    227216.. code-block:: genshitext
    228217
    229   #include '%s.txt' % filename
     218  {% include '%s.txt' % filename %}
    230219
    231220Note that a ``TemplateNotFound`` exception is raised if an included file can't
     
    238227================
    239228
    240 .. _`#with`:
    241 
    242 ``#with``
    243 -----------
    244 
    245 The ``#with`` directive lets you assign expressions to variables, which can
     229.. _`with`:
     230
     231``{% with %}``
     232--------------
     233
     234The ``{% with %}`` directive lets you assign expressions to variables, which can
    246235be used to make expressions inside the directive less verbose and more
    247236efficient. For example, if you need use the expression ``author.posts`` more
     
    254243
    255244  Magic numbers!
    256   #with y=7; z=x+10
     245  {% with y=7; z=x+10 %}
    257246    $x $y $z
    258   #end
     247  {% end %}
    259248
    260249Given ``x=42`` in the context data, this would produce::
     
    264253
    265254Note that if a variable of the same name already existed outside of the scope
    266 of the ``#with`` directive, it will **not** be overwritten. Instead, it will
    267 have the same value it had prior to the ``#with`` assignment. Effectively,
     255of the ``with`` directive, it will **not** be overwritten. Instead, it will
     256have the same value it had prior to the ``with`` assignment. Effectively,
    268257this means that variables are immutable in Genshi.
     258
     259
     260.. _whitespace:
     261
     262---------------------------
     263White-space and Line Breaks
     264---------------------------
     265
     266Note that space or line breaks around directives is never automatically removed.
     267Consider the following example:
     268
     269.. code-block:: genshitext
     270
     271  {% for item in items %}
     272    {% if item.visible %}
     273      ${item}
     274    {% end %}
     275  {% end %}
     276
     277This will result in two empty lines above and beneath every item, plus the
     278spaces used for indentation. If you want to supress a line break, simply end
     279the line with a backslash:
     280
     281.. code-block:: genshitext
     282
     283  {% for item in items %}\
     284    {% if item.visible %}\
     285      ${item}
     286    {% end %}\
     287  {% end %}\
     288
     289Now there would be no empty lines between the items in the output. But you still
     290get the spaces used for indentation, and because the line breaks are removed,
     291they actually continue and add up between lines. There are numerous ways to
     292control white-space in the output while keeping the template readable, such as
     293moving the indentation into the delimiters, or moving the end delimiter on the
     294next line, and so on.
    269295
    270296
     
    275301--------
    276302
    277 Lines where the first non-whitespace characters are ``##`` are removed from
    278 the output, and can thus be used for comments. This can be escaped using a
     303Parts in templates can be commented out using the delimiters ``{# ... #}``.
     304Any content in comments are removed from the output.
     305
     306.. code-block:: genshitext
     307
     308  {# This won't end up in the output #}
     309  This will.
     310
     311Just like directive delimiters, these can be escaped by prefixing with a
    279312backslash.
     313
     314.. code-block:: genshitext
     315
     316  \{# This *will* end up in the output, including delimiters #}
     317  This too.
     318
     319
     320.. _legacy:
     321
     322---------------------------
     323Legacy Text Template Syntax
     324---------------------------
     325
     326The syntax for text templates was redesigned in version 0.5 of Genshi to make
     327the language more flexible and powerful. The older syntax is based on line
     328starting with dollar signs, similar to e.g. Cheetah_ or Velocity_.
     329
     330.. _cheetah: http://cheetahtemplate.org/
     331.. _velocity: http://jakarta.apache.org/velocity/
     332
     333A simple template using the old syntax looked like this:
     334
     335.. code-block:: genshitext
     336
     337  Dear $name,
     338 
     339  We have the following items for you:
     340  #for item in items
     341   * $item
     342  #end
     343 
     344  All the best,
     345  Foobar
     346
     347Beyond the requirement of putting directives on separate lines prefixed with
     348dollar signs, the language itself is very similar to the new one. Except that
     349comments are lines that start with two ``#`` characters, and a line-break at the
     350end of a directive is removed automatically.
     351
     352.. note:: If you're using this old syntax, it is strongly recommended to migrate
     353          to the new syntax. Simply replace any references to ``TextTemplate``
     354          by ``NewTextTemplate``. On the other hand, if you want to stick with
     355          the old syntax for a while longer, replace references to
     356          ``TextTemplate`` by ``OldTextTemplate``; while ``TextTemplate`` is
     357          still an alias for the old language at this point, that will change
     358          in a future release.
  • trunk/examples/bench/basic.py

    r651 r706  
    1010import timeit
    1111
    12 __all__ = ['clearsilver', 'mako', 'django', 'kid', 'genshi', 'simpletal']
     12__all__ = ['clearsilver', 'mako', 'django', 'kid', 'genshi', 'genshi_text',
     13           'simpletal']
    1314
    1415def genshi(dirname, verbose=False):
     
    2021                    items=['Number %d' % num for num in range(1, 15)])
    2122        return template.generate(**data).render('xhtml')
     23
     24    if verbose:
     25        print render()
     26    return render
     27
     28def genshi_text(dirname, verbose=False):
     29    from genshi.core import escape
     30    from genshi.template import TemplateLoader, NewTextTemplate
     31    loader = TemplateLoader([dirname], auto_reload=False)
     32    template = loader.load('template.txt', cls=NewTextTemplate)
     33    def render():
     34        data = dict(escape=escape, title='Just a test', user='joe',
     35                    items=['Number %d' % num for num in range(1, 15)])
     36        return template.generate(**data).render('text')
    2237
    2338    if verbose:
  • trunk/examples/bench/bigtable.py

    r652 r706  
    1111from StringIO import StringIO
    1212from genshi.builder import tag
    13 from genshi.template import MarkupTemplate
     13from genshi.template import MarkupTemplate, NewTextTemplate
    1414
    1515try:
     
    5959genshi_tmpl2 = MarkupTemplate("""
    6060<table xmlns:py="http://genshi.edgewall.org/">$table</table>
     61""")
     62
     63genshi_text_tmpl = NewTextTemplate("""
     64<table>
     65{% for row in table %}<tr>
     66{% for c in row.values() %}<td>$c</td>{% end %}
     67</tr>{% end %}
     68</table>
    6169""")
    6270
     
    96104    stream.render('html', strip_whitespace=False)
    97105
     106def test_genshi_text():
     107    """Genshi text template"""
     108    stream = genshi_text_tmpl.generate(table=table)
     109    stream.render('text')
     110
    98111def test_genshi_builder():
    99112    """Genshi template + tag builder"""
     
    184197
    185198def run(which=None, number=10):
    186     tests = ['test_builder', 'test_genshi', 'test_genshi_builder',
    187              'test_mako', 'test_kid', 'test_kid_et', 'test_et', 'test_cet',
    188              'test_clearsilver', 'test_django']
     199    tests = ['test_builder', 'test_genshi', 'test_genshi_text',
     200             'test_genshi_builder', 'test_mako', 'test_kid', 'test_kid_et',
     201             'test_et', 'test_cet', 'test_clearsilver', 'test_django']
    189202
    190203    if which:
  • trunk/genshi/template/__init__.py

    r517 r706  
    1919from genshi.template.loader import TemplateLoader, TemplateNotFound
    2020from genshi.template.markup import MarkupTemplate
    21 from genshi.template.text import TextTemplate
     21from genshi.template.text import TextTemplate, OldTextTemplate, NewTextTemplate
    2222
    2323__docformat__ = 'restructuredtext en'
  • trunk/genshi/template/plugin.py

    r654 r706  
    2424from genshi.template.loader import TemplateLoader
    2525from genshi.template.markup import MarkupTemplate
    26 from genshi.template.text import TextTemplate
     26from genshi.template.text import TextTemplate, NewTextTemplate
    2727
    2828__all__ = ['ConfigurationError', 'AbstractTemplateEnginePlugin',
     
    163163    extension = '.txt'
    164164    default_format = 'text'
     165
     166    def __init__(self, extra_vars_func=None, options=None):
     167        if options is None:
     168            options = {}
     169
     170        new_syntax = options.get('genshi.new_text_syntax')
     171        if isinstance(new_syntax, basestring):
     172            new_syntax = new_syntax.lower() in ('1', 'on', 'yes', 'true')
     173        if new_syntax:
     174            self.template_class = NewTextTemplate
     175
     176        AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
  • trunk/genshi/template/tests/plugin.py

    r502 r706  
    1919from genshi.core import Stream
    2020from genshi.output import DocType
    21 from genshi.template import MarkupTemplate, TextTemplate
     21from genshi.template import MarkupTemplate, TextTemplate, NewTextTemplate
    2222from genshi.template.plugin import ConfigurationError, \
    2323                                   MarkupTemplateEnginePlugin, \
     
    186186        self.assertEqual('iso-8859-15', plugin.default_encoding)
    187187
     188    def test_init_with_new_syntax(self):
     189        plugin = TextTemplateEnginePlugin(options={
     190            'genshi.new_text_syntax': 'yes',
     191        })
     192        self.assertEqual(NewTextTemplate, plugin.template_class)
     193        tmpl = plugin.load_template(PACKAGE + '.templates.new_syntax')
     194        output = plugin.render({'foo': True}, template=tmpl)
     195        self.assertEqual('bar', output)
     196
    188197    def test_load_template_from_file(self):
    189198        plugin = TextTemplateEnginePlugin()
  • trunk/genshi/template/tests/text.py

    r616 r706  
    1919
    2020from genshi.template.loader import TemplateLoader
    21 from genshi.template.text import TextTemplate
    22 
    23 
    24 class TextTemplateTestCase(unittest.TestCase):
     21from genshi.template.text import OldTextTemplate, NewTextTemplate
     22
     23
     24class OldTextTemplateTestCase(unittest.TestCase):
    2525    """Tests for text template processing."""
    2626
     
    3232
    3333    def test_escaping(self):
    34         tmpl = TextTemplate('\\#escaped')
     34        tmpl = OldTextTemplate('\\#escaped')
    3535        self.assertEqual('#escaped', str(tmpl.generate()))
    3636
    3737    def test_comment(self):
    38         tmpl = TextTemplate('## a comment')
     38        tmpl = OldTextTemplate('## a comment')
    3939        self.assertEqual('', str(tmpl.generate()))
    4040
    4141    def test_comment_escaping(self):
    42         tmpl = TextTemplate('\\## escaped comment')
     42        tmpl = OldTextTemplate('\\## escaped comment')
    4343        self.assertEqual('## escaped comment', str(tmpl.generate()))
    4444
    4545    def test_end_with_args(self):
    46         tmpl = TextTemplate("""
     46        tmpl = OldTextTemplate("""
    4747        #if foo
    4848          bar
     
    5252    def test_latin1_encoded(self):
    5353        text = u'$foo\xf6$bar'.encode('iso-8859-1')
    54         tmpl = TextTemplate(text, encoding='iso-8859-1')
     54        tmpl = OldTextTemplate(text, encoding='iso-8859-1')
    5555        self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
    5656
    5757    def test_unicode_input(self):
    5858        text = u'$foo\xf6$bar'
    59         tmpl = TextTemplate(text)
     59        tmpl = OldTextTemplate(text)
    6060        self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
    6161
    6262    def test_empty_lines1(self):
    63         tmpl = TextTemplate("""Your items:
     63        tmpl = OldTextTemplate("""Your items:
    6464
    6565        #for item in items
     
    7474
    7575    def test_empty_lines2(self):
    76         tmpl = TextTemplate("""Your items:
     76        tmpl = OldTextTemplate("""Your items:
    7777
    7878        #for item in items
     
    106106
    107107        loader = TemplateLoader([self.dirname])
    108         tmpl = loader.load('tmpl2.txt', cls=TextTemplate)
     108        tmpl = loader.load('tmpl2.txt', cls=OldTextTemplate)
    109109        self.assertEqual("""----- Included data below this line -----
    110110Included
    111111            ----- Included data above this line -----""",
    112112                         tmpl.generate().render())
    113        
     113
     114
     115class NewTextTemplateTestCase(unittest.TestCase):
     116    """Tests for text template processing."""
     117
     118    def setUp(self):
     119        self.dirname = tempfile.mkdtemp(suffix='markup_test')
     120
     121    def tearDown(self):
     122        shutil.rmtree(self.dirname)
     123
     124    def test_escaping(self):
     125        tmpl = NewTextTemplate('\\{% escaped %}')
     126        self.assertEqual('{% escaped %}', str(tmpl.generate()))
     127
     128    def test_comment(self):
     129        tmpl = NewTextTemplate('{# a comment #}')
     130        self.assertEqual('', str(tmpl.generate()))
     131
     132    def test_comment_escaping(self):
     133        tmpl = NewTextTemplate('\\{# escaped comment #}')
     134        self.assertEqual('{# escaped comment #}', str(tmpl.generate()))
     135
     136    def test_end_with_args(self):
     137        tmpl = NewTextTemplate("""
     138{% if foo %}
     139  bar
     140{% end 'if foo' %}""")
     141        self.assertEqual('\n', str(tmpl.generate(foo=False)))
     142
     143    def test_latin1_encoded(self):
     144        text = u'$foo\xf6$bar'.encode('iso-8859-1')
     145        tmpl = NewTextTemplate(text, encoding='iso-8859-1')
     146        self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
     147
     148    def test_unicode_input(self):
     149        text = u'$foo\xf6$bar'
     150        tmpl = NewTextTemplate(text)
     151        self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
     152
     153    def test_empty_lines1(self):
     154        tmpl = NewTextTemplate("""Your items:
     155
     156{% for item in items %}\
     157  * ${item}
     158{% end %}""")
     159        self.assertEqual("""Your items:
     160
     161  * 0
     162  * 1
     163  * 2
     164""", tmpl.generate(items=range(3)).render('text'))
     165
     166    def test_empty_lines2(self):
     167        tmpl = NewTextTemplate("""Your items:
     168
     169{% for item in items %}\
     170  * ${item}
     171
     172{% end %}""")
     173        self.assertEqual("""Your items:
     174
     175  * 0
     176
     177  * 1
     178
     179  * 2
     180
     181""", tmpl.generate(items=range(3)).render('text'))
     182
     183    def test_include(self):
     184        file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w')
     185        try:
     186            file1.write("Included\n")
     187        finally:
     188            file1.close()
     189
     190        file2 = open(os.path.join(self.dirname, 'tmpl2.txt'), 'w')
     191        try:
     192            file2.write("""----- Included data below this line -----
     193{% include tmpl1.txt %}
     194----- Included data above this line -----""")
     195        finally:
     196            file2.close()
     197
     198        loader = TemplateLoader([self.dirname])
     199        tmpl = loader.load('tmpl2.txt', cls=NewTextTemplate)
     200        self.assertEqual("""----- Included data below this line -----
     201Included
     202----- Included data above this line -----""", tmpl.generate().render())
     203
     204
    114205def suite():
    115206    suite = unittest.TestSuite()
    116     suite.addTest(doctest.DocTestSuite(TextTemplate.__module__))
    117     suite.addTest(unittest.makeSuite(TextTemplateTestCase, 'test'))
     207    suite.addTest(doctest.DocTestSuite(NewTextTemplate.__module__))
     208    suite.addTest(unittest.makeSuite(OldTextTemplateTestCase, 'test'))
     209    suite.addTest(unittest.makeSuite(NewTextTemplateTestCase, 'test'))
    118210    return suite
    119211
  • trunk/genshi/template/text.py

    r616 r706  
    1212# history and logs, available at http://genshi.edgewall.org/log/.
    1313
    14 """Plain text templating engine."""
     14"""Plain text templating engine.
     15
     16This module implements two template language syntaxes, at least for a certain
     17transitional period. `OldTextTemplate` (aliased to just `TextTemplate`) defines
     18a syntax that was inspired by Cheetah/Velocity. `NewTextTemplate` on the other
     19hand is inspired by the syntax of the Django template language, which has more
     20explicit delimiting of directives, and is more flexible with regards to
     21white space and line breaks.
     22
     23In a future release, `OldTextTemplate` will be phased out in favor of
     24`NewTextTemplate`, as the names imply. Therefore the new syntax is strongly
     25recommended for new projects, and existing projects may want to migrate to the
     26new syntax to remain compatible with future Genshi releases.
     27"""
    1528
    1629import re
     
    2134from genshi.template.interpolation import interpolate
    2235
    23 __all__ = ['TextTemplate']
     36__all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate']
    2437__docformat__ = 'restructuredtext en'
    2538
    2639
    27 class TextTemplate(Template):
    28     """Implementation of a simple text-based template engine.
    29    
    30     >>> tmpl = TextTemplate('''Dear $name,
     40class NewTextTemplate(Template):
     41    r"""Implementation of a simple text-based template engine. This class will
     42    replace `OldTextTemplate` in a future release.
     43   
     44    It uses a more explicit delimiting style for directives: instead of the old
     45    style which required putting directives on separate lines that were prefixed
     46    with a ``#`` sign, directives and commenbtsr are enclosed in delimiter pairs
     47    (by default ``{% ... %}`` and ``{# ... #}``, respectively).
     48   
     49    Variable substitution uses the same interpolation syntax as for markup
     50    languages: simple references are prefixed with a dollar sign, more complex
     51    expression enclosed in curly braces.
     52   
     53    >>> tmpl = NewTextTemplate('''Dear $name,
     54    ...
     55    ... {# This is a comment #}
     56    ... We have the following items for you:
     57    ... {% for item in items %}
     58    ...  * ${'Item %d' % item}
     59    ... {% end %}
     60    ... ''')
     61    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
     62    Dear Joe,
     63    <BLANKLINE>
     64    <BLANKLINE>
     65    We have the following items for you:
     66    <BLANKLINE>
     67     * Item 1
     68    <BLANKLINE>
     69     * Item 2
     70    <BLANKLINE>
     71     * Item 3
     72    <BLANKLINE>
     73    <BLANKLINE>
     74   
     75    By default, no spaces or line breaks are removed. If a line break should
     76    not be included in the output, prefix it with a backslash:
     77   
     78    >>> tmpl = NewTextTemplate('''Dear $name,
     79    ...
     80    ... {# This is a comment #}\
     81    ... We have the following items for you:
     82    ... {% for item in items %}\
     83    ...  * $item
     84    ... {% end %}\
     85    ... ''')
     86    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
     87    Dear Joe,
     88    <BLANKLINE>
     89    We have the following items for you:
     90     * 1
     91     * 2
     92     * 3
     93    <BLANKLINE>
     94   
     95    Backslashes are also used to escape the start delimiter of directives and
     96    comments:
     97
     98    >>> tmpl = NewTextTemplate('''Dear $name,
     99    ...
     100    ... \{# This is a comment #}
     101    ... We have the following items for you:
     102    ... {% for item in items %}\
     103    ...  * $item
     104    ... {% end %}\
     105    ... ''')
     106    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
     107    Dear Joe,
     108    <BLANKLINE>
     109    {# This is a comment #}
     110    We have the following items for you:
     111     * 1
     112     * 2
     113     * 3
     114    <BLANKLINE>
     115   
     116    :since: version 0.5
     117    """
     118    directives = [('def', DefDirective),
     119                  ('when', WhenDirective),
     120                  ('otherwise', OtherwiseDirective),
     121                  ('for', ForDirective),
     122                  ('if', IfDirective),
     123                  ('choose', ChooseDirective),
     124                  ('with', WithDirective)]
     125
     126    _DIRECTIVE_RE = r'((?<!\\)%s\s*(\w+)\s*(.*?)\s*%s|(?<!\\)%s.*?%s)'
     127    _ESCAPE_RE = r'\\\n|\\(\\)|\\(%s)|\\(%s)'
     128
     129    def __init__(self, source, basedir=None, filename=None, loader=None,
     130                 encoding=None, lookup='lenient', allow_exec=False,
     131                 delims=('{%', '%}', '{#', '#}')):
     132        self.delimiters = delims
     133        Template.__init__(self, source, basedir=basedir, filename=filename,
     134                          loader=loader, encoding=encoding, lookup=lookup)
     135
     136    def _get_delims(self):
     137        return self._delims
     138    def _set_delims(self, delims):
     139        if len(delims) != 4:
     140            raise ValueError('delimiers tuple must have exactly four elements')
     141        self._delims = delims
     142        self._directive_re = re.compile(self._DIRECTIVE_RE % tuple(
     143            map(re.escape, delims)
     144        ))
     145        self._escape_re = re.compile(self._ESCAPE_RE % tuple(
     146            map(re.escape, delims[::2])
     147        ))
     148    delimiters = property(_get_delims, _set_delims, """\
     149    The delimiters for directives and comments. This should be a four item tuple
     150    of the form ``(directive_start, directive_end, comment_start,
     151    comment_end)``, where each item is a string.
     152    """)
     153
     154    def _parse(self, source, encoding):
     155        """Parse the template from text input."""
     156        stream = [] # list of events of the "compiled" template
     157        dirmap = {} # temporary mapping of directives to elements
     158        depth = 0
     159
     160        source = source.read()
     161        if isinstance(source, str):
     162            source = source.decode(encoding or 'utf-8', 'replace')
     163        offset = 0
     164        lineno = 1
     165
     166        _escape_sub = self._escape_re.sub
     167        def _escape_repl(mo):
     168            groups = filter(None, mo.groups())
     169            if not groups:
     170                return ''
     171            return groups[0]
     172
     173        for idx, mo in enumerate(self._directive_re.finditer(source)):
     174            start, end = mo.span(1)
     175            if start > offset:
     176                text = _escape_sub(_escape_repl, source[offset:start])
     177                for kind, data, pos in interpolate(text, self.basedir,
     178                                                   self.filename, lineno,
     179                                                   lookup=self.lookup):
     180                    stream.append((kind, data, pos))
     181                lineno += len(text.splitlines())
     182
     183            lineno += len(source[start:end].splitlines())
     184            command, value = mo.group(2, 3)
     185            if command:
     186                if command == 'include':
     187                    pos = (self.filename, lineno, 0)
     188                    stream.append((INCLUDE, (value.strip(), []), pos))
     189                elif command == 'end':
     190                    depth -= 1
     191                    if depth in dirmap:
     192                        directive, start_offset = dirmap.pop(depth)
     193                        substream = stream[start_offset:]
     194                        stream[start_offset:] = [(SUB, ([directive], substream),
     195                                                  (self.filepath, lineno, 0))]
     196                else:
     197                    cls = self._dir_by_name.get(command)
     198                    if cls is None:
     199                        raise BadDirectiveError(command)
     200                    directive = cls, value, None, (self.filepath, lineno, 0)
     201                    dirmap[depth] = (directive, len(stream))
     202                    depth += 1
     203
     204            offset = end
     205
     206        if offset < len(source):
     207            text = _escape_sub(_escape_repl, source[offset:])
     208            for kind, data, pos in interpolate(text, self.basedir,
     209                                               self.filename, lineno,
     210                                               lookup=self.lookup):
     211                stream.append((kind, data, pos))
     212
     213        return stream
     214
     215
     216class OldTextTemplate(Template):
     217    """Legacy implementation of the old syntax text-based templates. This class
     218    is provided in a transition phase for backwards compatibility. New code
     219    should use the `NewTextTemplate` class and the improved syntax it provides.
     220   
     221    >>> tmpl = OldTextTemplate('''Dear $name,
    31222    ...
    32223    ... We have the following items for you:
     
    118309
    119310        return stream
     311
     312
     313TextTemplate = OldTextTemplate
Note: See TracChangeset for help on using the changeset viewer.