Edgewall Software

source: trunk/genshi/template/text.py

Last change on this file was 1255, checked in by hodgestar, 10 years ago

Support slash escaped of CRLF newlines (fixes #569; patch from tetsuya.morimoto@…).

  • Property svn:eol-style set to native
File size: 12.2 KB
RevLine 
[414]1# -*- coding: utf-8 -*-
2#
[1069]3# Copyright (C) 2006-2009 Edgewall Software
[414]4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
[706]14"""Plain text templating engine.
[414]15
[706]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"""
28
[414]29import re
30
[809]31from genshi.core import TEXT
[726]32from genshi.template.base import BadDirectiveError, Template, \
33                                 TemplateSyntaxError, EXEC, INCLUDE, SUB
[725]34from genshi.template.eval import Suite
[414]35from genshi.template.directives import *
[816]36from genshi.template.directives import Directive
[499]37from genshi.template.interpolation import interpolate
[414]38
[706]39__all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate']
[517]40__docformat__ = 'restructuredtext en'
[414]41
[517]42
[706]43class NewTextTemplate(Template):
44    r"""Implementation of a simple text-based template engine. This class will
45    replace `OldTextTemplate` in a future release.
[414]46   
[706]47    It uses a more explicit delimiting style for directives: instead of the old
48    style which required putting directives on separate lines that were prefixed
49    with a ``#`` sign, directives and commenbtsr are enclosed in delimiter pairs
50    (by default ``{% ... %}`` and ``{# ... #}``, respectively).
51   
52    Variable substitution uses the same interpolation syntax as for markup
53    languages: simple references are prefixed with a dollar sign, more complex
54    expression enclosed in curly braces.
55   
56    >>> tmpl = NewTextTemplate('''Dear $name,
[414]57    ...
[706]58    ... {# This is a comment #}
[414]59    ... We have the following items for you:
[706]60    ... {% for item in items %}
61    ...  * ${'Item %d' % item}
62    ... {% end %}
63    ... ''')
[1088]64    >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
[706]65    Dear Joe,
66    <BLANKLINE>
67    <BLANKLINE>
68    We have the following items for you:
69    <BLANKLINE>
70     * Item 1
71    <BLANKLINE>
72     * Item 2
73    <BLANKLINE>
74     * Item 3
75    <BLANKLINE>
76    <BLANKLINE>
77   
78    By default, no spaces or line breaks are removed. If a line break should
79    not be included in the output, prefix it with a backslash:
80   
81    >>> tmpl = NewTextTemplate('''Dear $name,
82    ...
83    ... {# This is a comment #}\
84    ... We have the following items for you:
85    ... {% for item in items %}\
86    ...  * $item
87    ... {% end %}\
88    ... ''')
[1088]89    >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
[706]90    Dear Joe,
91    <BLANKLINE>
92    We have the following items for you:
93     * 1
94     * 2
95     * 3
96    <BLANKLINE>
97   
98    Backslashes are also used to escape the start delimiter of directives and
99    comments:
100
101    >>> tmpl = NewTextTemplate('''Dear $name,
102    ...
103    ... \{# This is a comment #}
104    ... We have the following items for you:
105    ... {% for item in items %}\
106    ...  * $item
107    ... {% end %}\
108    ... ''')
[1088]109    >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
[706]110    Dear Joe,
111    <BLANKLINE>
112    {# This is a comment #}
113    We have the following items for you:
114     * 1
115     * 2
116     * 3
117    <BLANKLINE>
118   
119    :since: version 0.5
120    """
121    directives = [('def', DefDirective),
122                  ('when', WhenDirective),
123                  ('otherwise', OtherwiseDirective),
124                  ('for', ForDirective),
125                  ('if', IfDirective),
126                  ('choose', ChooseDirective),
127                  ('with', WithDirective)]
[721]128    serializer = 'text'
[706]129
130    _DIRECTIVE_RE = r'((?<!\\)%s\s*(\w+)\s*(.*?)\s*%s|(?<!\\)%s.*?%s)'
[1255]131    _ESCAPE_RE = r'\\\n|\\\r\n|\\(\\)|\\(%s)|\\(%s)'
[706]132
[830]133    def __init__(self, source, filepath=None, filename=None, loader=None,
[722]134                 encoding=None, lookup='strict', allow_exec=False,
[706]135                 delims=('{%', '%}', '{#', '#}')):
136        self.delimiters = delims
[830]137        Template.__init__(self, source, filepath=filepath, filename=filename,
[706]138                          loader=loader, encoding=encoding, lookup=lookup)
139
140    def _get_delims(self):
141        return self._delims
142    def _set_delims(self, delims):
143        if len(delims) != 4:
144            raise ValueError('delimiers tuple must have exactly four elements')
145        self._delims = delims
146        self._directive_re = re.compile(self._DIRECTIVE_RE % tuple(
[1077]147            [re.escape(d) for d in delims]
[725]148        ), re.DOTALL)
[706]149        self._escape_re = re.compile(self._ESCAPE_RE % tuple(
[1077]150            [re.escape(d) for d in delims[::2]]
[706]151        ))
152    delimiters = property(_get_delims, _set_delims, """\
153    The delimiters for directives and comments. This should be a four item tuple
154    of the form ``(directive_start, directive_end, comment_start,
155    comment_end)``, where each item is a string.
156    """)
157
158    def _parse(self, source, encoding):
159        """Parse the template from text input."""
160        stream = [] # list of events of the "compiled" template
161        dirmap = {} # temporary mapping of directives to elements
162        depth = 0
163
164        source = source.read()
[1160]165        if not isinstance(source, unicode):
[706]166            source = source.decode(encoding or 'utf-8', 'replace')
167        offset = 0
168        lineno = 1
169
170        _escape_sub = self._escape_re.sub
171        def _escape_repl(mo):
[1081]172            groups = [g for g in mo.groups() if g]
[706]173            if not groups:
174                return ''
175            return groups[0]
176
177        for idx, mo in enumerate(self._directive_re.finditer(source)):
178            start, end = mo.span(1)
179            if start > offset:
180                text = _escape_sub(_escape_repl, source[offset:start])
[830]181                for kind, data, pos in interpolate(text, self.filepath, lineno,
[706]182                                                   lookup=self.lookup):
183                    stream.append((kind, data, pos))
184                lineno += len(text.splitlines())
185
186            lineno += len(source[start:end].splitlines())
187            command, value = mo.group(2, 3)
188
[725]189            if command == 'include':
190                pos = (self.filename, lineno, 0)
[830]191                value = list(interpolate(value, self.filepath, lineno, 0,
192                                         lookup=self.lookup))
[809]193                if len(value) == 1 and value[0][0] is TEXT:
194                    value = value[0][1]
195                stream.append((INCLUDE, (value, None, []), pos))
[725]196
197            elif command == 'python':
198                if not self.allow_exec:
199                    raise TemplateSyntaxError('Python code blocks not allowed',
200                                              self.filepath, lineno)
201                try:
202                    suite = Suite(value, self.filepath, lineno,
203                                  lookup=self.lookup)
204                except SyntaxError, err:
205                    raise TemplateSyntaxError(err, self.filepath,
206                                              lineno + (err.lineno or 1) - 1)
207                pos = (self.filename, lineno, 0)
208                stream.append((EXEC, suite, pos))
209
210            elif command == 'end':
211                depth -= 1
212                if depth in dirmap:
213                    directive, start_offset = dirmap.pop(depth)
214                    substream = stream[start_offset:]
215                    stream[start_offset:] = [(SUB, ([directive], substream),
216                                              (self.filepath, lineno, 0))]
217
218            elif command:
[954]219                cls = self.get_directive(command)
[725]220                if cls is None:
221                    raise BadDirectiveError(command)
[1069]222                directive = 0, cls, value, None, (self.filepath, lineno, 0)
[725]223                dirmap[depth] = (directive, len(stream))
224                depth += 1
225
[706]226            offset = end
227
228        if offset < len(source):
229            text = _escape_sub(_escape_repl, source[offset:])
[830]230            for kind, data, pos in interpolate(text, self.filepath, lineno,
[706]231                                               lookup=self.lookup):
232                stream.append((kind, data, pos))
233
234        return stream
235
236
237class OldTextTemplate(Template):
238    """Legacy implementation of the old syntax text-based templates. This class
239    is provided in a transition phase for backwards compatibility. New code
240    should use the `NewTextTemplate` class and the improved syntax it provides.
241   
242    >>> tmpl = OldTextTemplate('''Dear $name,
243    ...
244    ... We have the following items for you:
[414]245    ... #for item in items
246    ...  * $item
247    ... #end
248    ...
249    ... All the best,
250    ... Foobar''')
[1088]251    >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
[414]252    Dear Joe,
253    <BLANKLINE>
254    We have the following items for you:
255     * 1
256     * 2
257     * 3
258    <BLANKLINE>
259    All the best,
260    Foobar
261    """
262    directives = [('def', DefDirective),
263                  ('when', WhenDirective),
264                  ('otherwise', OtherwiseDirective),
265                  ('for', ForDirective),
266                  ('if', IfDirective),
267                  ('choose', ChooseDirective),
268                  ('with', WithDirective)]
[721]269    serializer = 'text'
[414]270
[445]271    _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(?<!\\)#(end).*\n?)|'
272                               r'(?:^[ \t]*(?<!\\)#((?:\w+|#).*)\n?)',
273                               re.MULTILINE)
[414]274
[456]275    def _parse(self, source, encoding):
[414]276        """Parse the template from text input."""
277        stream = [] # list of events of the "compiled" template
278        dirmap = {} # temporary mapping of directives to elements
279        depth = 0
280
[616]281        source = source.read()
[1160]282        if not isinstance(source, unicode):
[616]283            source = source.decode(encoding or 'utf-8', 'replace')
[414]284        offset = 0
285        lineno = 1
286
287        for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)):
288            start, end = mo.span()
289            if start > offset:
290                text = source[offset:start]
[830]291                for kind, data, pos in interpolate(text, self.filepath, lineno,
[534]292                                                   lookup=self.lookup):
[414]293                    stream.append((kind, data, pos))
294                lineno += len(text.splitlines())
295
296            text = source[start:end].lstrip()[1:]
297            lineno += len(text.splitlines())
298            directive = text.split(None, 1)
299            if len(directive) > 1:
300                command, value = directive
301            else:
302                command, value = directive[0], None
303
304            if command == 'end':
305                depth -= 1
306                if depth in dirmap:
307                    directive, start_offset = dirmap.pop(depth)
308                    substream = stream[start_offset:]
309                    stream[start_offset:] = [(SUB, ([directive], substream),
310                                              (self.filepath, lineno, 0))]
[575]311            elif command == 'include':
312                pos = (self.filename, lineno, 0)
[726]313                stream.append((INCLUDE, (value.strip(), None, []), pos))
[414]314            elif command != '#':
[954]315                cls = self.get_directive(command)
[414]316                if cls is None:
317                    raise BadDirectiveError(command)
[1069]318                directive = 0, cls, value, None, (self.filepath, lineno, 0)
[414]319                dirmap[depth] = (directive, len(stream))
320                depth += 1
321
322            offset = end
323
324        if offset < len(source):
325            text = source[offset:].replace('\\#', '#')
[830]326            for kind, data, pos in interpolate(text, self.filepath, lineno,
[534]327                                               lookup=self.lookup):
[414]328                stream.append((kind, data, pos))
329
330        return stream
[706]331
332
333TextTemplate = OldTextTemplate
Note: See TracBrowser for help on using the repository browser.