1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2009 Edgewall Software
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
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"""
28
29import re
30
31from genshi.compat import text_type
32from genshi.core import TEXT
33from genshi.template.base import BadDirectiveError, Template, \
34 TemplateSyntaxError, EXEC, INCLUDE, SUB
35from genshi.template.eval import Suite
36from genshi.template.directives import *
37from genshi.template.interpolation import interpolate
38
39__all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate']
40__docformat__ = 'restructuredtext en'
41
42
43class NewTextTemplate(Template):
44 r"""Implementation of a simple text-based template engine. This class will
45 replace `OldTextTemplate` in a future release.
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(encoding=None))
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(encoding=None))
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(encoding=None))
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|\\\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 [re.escape(d) for d in delims]
148 ), re.DOTALL)
149 self._escape_re = re.compile(self._ESCAPE_RE % tuple(
150 [re.escape(d) for d in 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 not isinstance(source, text_type):
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 = [g for g in mo.groups() if g]
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 as 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.get_directive(command)
220 if cls is None:
221 raise BadDirectiveError(command)
222 directive = 0, 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
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:
245 ... #for item in items
246 ... * $item
247 ... #end
248 ...
249 ... All the best,
250 ... Foobar''')
251 >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
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)]
269 serializer = 'text'
270
271 _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(?<!\\)#(end).*\n?)|'
272 r'(?:^[ \t]*(?<!\\)#((?:\w+|#).*)\n?)',
273 re.MULTILINE)
274
275 def _parse(self, source, encoding):
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
281 source = source.read()
282 if not isinstance(source, text_type):
283 source = source.decode(encoding or 'utf-8', 'replace')
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]
291 for kind, data, pos in interpolate(text, self.filepath, lineno,
292 lookup=self.lookup):
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))]
311 elif command == 'include':
312 pos = (self.filename, lineno, 0)
313 stream.append((INCLUDE, (value.strip(), None, []), pos))
314 elif command != '#':
315 cls = self.get_directive(command)
316 if cls is None:
317 raise BadDirectiveError(command)
318 directive = 0, cls, value, None, (self.filepath, lineno, 0)
319 dirmap[depth] = (directive, len(stream))
320 depth += 1
321
322 offset = end
323
324 if offset < len(source):
325 text = source[offset:].replace('\\#', '#')
326 for kind, data, pos in interpolate(text, self.filepath, lineno,
327 lookup=self.lookup):
328 stream.append((kind, data, pos))
329
330 return stream
331
332
333TextTemplate = OldTextTemplate