comparison genshi/template/text.py @ 720:acf7c5ee36e7 experimental-newctxt

newctxt branch: Merged revisions [678:835] via svnmerge from [source:trunk].
author cmlenz
date Fri, 11 Apr 2008 08:42:11 +0000
parents ed9f1300f3bf
children
comparison
equal deleted inserted replaced
567:837786a584d5 720:acf7c5ee36e7
1 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 # 2 #
3 # Copyright (C) 2006-2007 Edgewall Software 3 # Copyright (C) 2006-2008 Edgewall Software
4 # All rights reserved. 4 # All rights reserved.
5 # 5 #
6 # This software is licensed as described in the file COPYING, which 6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms 7 # you should have received as part of this distribution. The terms
8 # are also available at http://genshi.edgewall.org/wiki/License. 8 # are also available at http://genshi.edgewall.org/wiki/License.
9 # 9 #
10 # This software consists of voluntary contributions made by many 10 # This software consists of voluntary contributions made by many
11 # individuals. For the exact contribution history, see the revision 11 # individuals. For the exact contribution history, see the revision
12 # history and logs, available at http://genshi.edgewall.org/log/. 12 # history and logs, available at http://genshi.edgewall.org/log/.
13 13
14 """Plain text templating engine.""" 14 """Plain text templating engine.
15
16 This module implements two template language syntaxes, at least for a certain
17 transitional period. `OldTextTemplate` (aliased to just `TextTemplate`) defines
18 a syntax that was inspired by Cheetah/Velocity. `NewTextTemplate` on the other
19 hand is inspired by the syntax of the Django template language, which has more
20 explicit delimiting of directives, and is more flexible with regards to
21 white space and line breaks.
22
23 In a future release, `OldTextTemplate` will be phased out in favor of
24 `NewTextTemplate`, as the names imply. Therefore the new syntax is strongly
25 recommended for new projects, and existing projects may want to migrate to the
26 new syntax to remain compatible with future Genshi releases.
27 """
15 28
16 import re 29 import re
17 30
18 from genshi.template.base import BadDirectiveError, Template, INCLUDE, SUB 31 from genshi.core import TEXT
32 from genshi.template.base import BadDirectiveError, Template, \
33 TemplateSyntaxError, EXEC, INCLUDE, SUB
34 from genshi.template.eval import Suite
19 from genshi.template.directives import * 35 from genshi.template.directives import *
20 from genshi.template.directives import Directive, _apply_directives 36 from genshi.template.directives import Directive
21 from genshi.template.interpolation import interpolate 37 from genshi.template.interpolation import interpolate
22 38
23 __all__ = ['TextTemplate'] 39 __all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate']
24 __docformat__ = 'restructuredtext en' 40 __docformat__ = 'restructuredtext en'
25 41
26 42
27 class TextTemplate(Template): 43 class NewTextTemplate(Template):
28 """Implementation of a simple text-based template engine. 44 r"""Implementation of a simple text-based template engine. This class will
29 45 replace `OldTextTemplate` in a future release.
30 >>> tmpl = TextTemplate('''Dear $name, 46
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,
57 ...
58 ... {# This is a comment #}
59 ... We have the following items for you:
60 ... {% for item in items %}
61 ... * ${'Item %d' % item}
62 ... {% end %}
63 ... ''')
64 >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render()
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 ... ''')
89 >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render()
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 ... ''')
109 >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render()
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)]
128 serializer = 'text'
129
130 _DIRECTIVE_RE = r'((?<!\\)%s\s*(\w+)\s*(.*?)\s*%s|(?<!\\)%s.*?%s)'
131 _ESCAPE_RE = r'\\\n|\\(\\)|\\(%s)|\\(%s)'
132
133 def __init__(self, source, filepath=None, filename=None, loader=None,
134 encoding=None, lookup='strict', allow_exec=False,
135 delims=('{%', '%}', '{#', '#}')):
136 self.delimiters = delims
137 Template.__init__(self, source, filepath=filepath, filename=filename,
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(
147 map(re.escape, delims)
148 ), re.DOTALL)
149 self._escape_re = re.compile(self._ESCAPE_RE % tuple(
150 map(re.escape, delims[::2])
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()
165 if isinstance(source, str):
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):
172 groups = filter(None, mo.groups())
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])
181 for kind, data, pos in interpolate(text, self.filepath, lineno,
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
189 if command == 'include':
190 pos = (self.filename, lineno, 0)
191 value = list(interpolate(value, self.filepath, lineno, 0,
192 lookup=self.lookup))
193 if len(value) == 1 and value[0][0] is TEXT:
194 value = value[0][1]
195 stream.append((INCLUDE, (value, None, []), pos))
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:
219 cls = self._dir_by_name.get(command)
220 if cls is None:
221 raise BadDirectiveError(command)
222 directive = cls, value, None, (self.filepath, lineno, 0)
223 dirmap[depth] = (directive, len(stream))
224 depth += 1
225
226 offset = end
227
228 if offset < len(source):
229 text = _escape_sub(_escape_repl, source[offset:])
230 for kind, data, pos in interpolate(text, self.filepath, lineno,
231 lookup=self.lookup):
232 stream.append((kind, data, pos))
233
234 return stream
235
236
237 class 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,
31 ... 243 ...
32 ... We have the following items for you: 244 ... We have the following items for you:
33 ... #for item in items 245 ... #for item in items
34 ... * $item 246 ... * $item
35 ... #end 247 ... #end
36 ... 248 ...
37 ... All the best, 249 ... All the best,
38 ... Foobar''') 250 ... Foobar''')
39 >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text') 251 >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render()
40 Dear Joe, 252 Dear Joe,
41 <BLANKLINE> 253 <BLANKLINE>
42 We have the following items for you: 254 We have the following items for you:
43 * 1 255 * 1
44 * 2 256 * 2
52 ('otherwise', OtherwiseDirective), 264 ('otherwise', OtherwiseDirective),
53 ('for', ForDirective), 265 ('for', ForDirective),
54 ('if', IfDirective), 266 ('if', IfDirective),
55 ('choose', ChooseDirective), 267 ('choose', ChooseDirective),
56 ('with', WithDirective)] 268 ('with', WithDirective)]
269 serializer = 'text'
57 270
58 _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(?<!\\)#(end).*\n?)|' 271 _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(?<!\\)#(end).*\n?)|'
59 r'(?:^[ \t]*(?<!\\)#((?:\w+|#).*)\n?)', 272 r'(?:^[ \t]*(?<!\\)#((?:\w+|#).*)\n?)',
60 re.MULTILINE) 273 re.MULTILINE)
61 274
73 286
74 for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)): 287 for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)):
75 start, end = mo.span() 288 start, end = mo.span()
76 if start > offset: 289 if start > offset:
77 text = source[offset:start] 290 text = source[offset:start]
78 for kind, data, pos in interpolate(text, self.basedir, 291 for kind, data, pos in interpolate(text, self.filepath, lineno,
79 self.filename, lineno,
80 lookup=self.lookup): 292 lookup=self.lookup):
81 stream.append((kind, data, pos)) 293 stream.append((kind, data, pos))
82 lineno += len(text.splitlines()) 294 lineno += len(text.splitlines())
83 295
84 text = source[start:end].lstrip()[1:] 296 text = source[start:end].lstrip()[1:]
96 substream = stream[start_offset:] 308 substream = stream[start_offset:]
97 stream[start_offset:] = [(SUB, ([directive], substream), 309 stream[start_offset:] = [(SUB, ([directive], substream),
98 (self.filepath, lineno, 0))] 310 (self.filepath, lineno, 0))]
99 elif command == 'include': 311 elif command == 'include':
100 pos = (self.filename, lineno, 0) 312 pos = (self.filename, lineno, 0)
101 stream.append((INCLUDE, (value.strip(), []), pos)) 313 stream.append((INCLUDE, (value.strip(), None, []), pos))
102 elif command != '#': 314 elif command != '#':
103 cls = self._dir_by_name.get(command) 315 cls = self._dir_by_name.get(command)
104 if cls is None: 316 if cls is None:
105 raise BadDirectiveError(command) 317 raise BadDirectiveError(command)
106 directive = cls, value, None, (self.filepath, lineno, 0) 318 directive = cls, value, None, (self.filepath, lineno, 0)
109 321
110 offset = end 322 offset = end
111 323
112 if offset < len(source): 324 if offset < len(source):
113 text = source[offset:].replace('\\#', '#') 325 text = source[offset:].replace('\\#', '#')
114 for kind, data, pos in interpolate(text, self.basedir, 326 for kind, data, pos in interpolate(text, self.filepath, lineno,
115 self.filename, lineno,
116 lookup=self.lookup): 327 lookup=self.lookup):
117 stream.append((kind, data, pos)) 328 stream.append((kind, data, pos))
118 329
119 return stream 330 return stream
331
332
333 TextTemplate = OldTextTemplate
Copyright (C) 2012-2017 Edgewall Software