1 | # -*- coding: utf-8 -*- |
---|
2 | # |
---|
3 | # Copyright (C) 2006 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 | """Implementation of the template engine.""" |
---|
15 | |
---|
16 | from itertools import chain |
---|
17 | try: |
---|
18 | from collections import deque |
---|
19 | except ImportError: |
---|
20 | class deque(list): |
---|
21 | def appendleft(self, x): self.insert(0, x) |
---|
22 | def popleft(self): return self.pop(0) |
---|
23 | import compiler |
---|
24 | import os |
---|
25 | import re |
---|
26 | from StringIO import StringIO |
---|
27 | |
---|
28 | from genshi.core import Attrs, Namespace, Stream, StreamEventKind, _ensure |
---|
29 | from genshi.core import START, END, START_NS, END_NS, TEXT, COMMENT |
---|
30 | from genshi.eval import Expression, _parse |
---|
31 | from genshi.input import XMLParser |
---|
32 | from genshi.path import Path |
---|
33 | |
---|
34 | __all__ = ['BadDirectiveError', 'MarkupTemplate', 'Template', 'TemplateError', |
---|
35 | 'TemplateSyntaxError', 'TemplateNotFound', 'TemplateLoader', |
---|
36 | 'TextTemplate'] |
---|
37 | |
---|
38 | |
---|
39 | class TemplateError(Exception): |
---|
40 | """Base exception class for errors related to template processing.""" |
---|
41 | |
---|
42 | |
---|
43 | class TemplateSyntaxError(TemplateError): |
---|
44 | """Exception raised when an expression in a template causes a Python syntax |
---|
45 | error.""" |
---|
46 | |
---|
47 | def __init__(self, message, filename='<string>', lineno=-1, offset=-1): |
---|
48 | if isinstance(message, SyntaxError) and message.lineno is not None: |
---|
49 | message = str(message).replace(' (line %d)' % message.lineno, '') |
---|
50 | self.msg = message |
---|
51 | message = '%s (%s, line %d)' % (self.msg, filename, lineno) |
---|
52 | TemplateError.__init__(self, message) |
---|
53 | self.filename = filename |
---|
54 | self.lineno = lineno |
---|
55 | self.offset = offset |
---|
56 | |
---|
57 | |
---|
58 | class BadDirectiveError(TemplateSyntaxError): |
---|
59 | """Exception raised when an unknown directive is encountered when parsing |
---|
60 | a template. |
---|
61 | |
---|
62 | An unknown directive is any attribute using the namespace for directives, |
---|
63 | with a local name that doesn't match any registered directive. |
---|
64 | """ |
---|
65 | |
---|
66 | def __init__(self, name, filename='<string>', lineno=-1): |
---|
67 | message = 'bad directive "%s"' % name |
---|
68 | TemplateSyntaxError.__init__(self, message, filename, lineno) |
---|
69 | |
---|
70 | |
---|
71 | class TemplateRuntimeError(TemplateError): |
---|
72 | """Exception raised when an the evualation of a Python expression in a |
---|
73 | template causes an error.""" |
---|
74 | |
---|
75 | def __init__(self, message, filename='<string>', lineno=-1, offset=-1): |
---|
76 | self.msg = message |
---|
77 | message = '%s (%s, line %d)' % (self.msg, filename, lineno) |
---|
78 | TemplateError.__init__(self, message) |
---|
79 | self.filename = filename |
---|
80 | self.lineno = lineno |
---|
81 | self.offset = offset |
---|
82 | |
---|
83 | |
---|
84 | class TemplateNotFound(TemplateError): |
---|
85 | """Exception raised when a specific template file could not be found.""" |
---|
86 | |
---|
87 | def __init__(self, name, search_path): |
---|
88 | TemplateError.__init__(self, 'Template "%s" not found' % name) |
---|
89 | self.search_path = search_path |
---|
90 | |
---|
91 | |
---|
92 | class Context(object): |
---|
93 | """Container for template input data. |
---|
94 | |
---|
95 | A context provides a stack of scopes (represented by dictionaries). |
---|
96 | |
---|
97 | Template directives such as loops can push a new scope on the stack with |
---|
98 | data that should only be available inside the loop. When the loop |
---|
99 | terminates, that scope can get popped off the stack again. |
---|
100 | |
---|
101 | >>> ctxt = Context(one='foo', other=1) |
---|
102 | >>> ctxt.get('one') |
---|
103 | 'foo' |
---|
104 | >>> ctxt.get('other') |
---|
105 | 1 |
---|
106 | >>> ctxt.push(dict(one='frost')) |
---|
107 | >>> ctxt.get('one') |
---|
108 | 'frost' |
---|
109 | >>> ctxt.get('other') |
---|
110 | 1 |
---|
111 | >>> ctxt.pop() |
---|
112 | {'one': 'frost'} |
---|
113 | >>> ctxt.get('one') |
---|
114 | 'foo' |
---|
115 | """ |
---|
116 | |
---|
117 | def __init__(self, **data): |
---|
118 | self.frames = deque([data]) |
---|
119 | self.pop = self.frames.popleft |
---|
120 | self.push = self.frames.appendleft |
---|
121 | self._match_templates = [] |
---|
122 | |
---|
123 | def __repr__(self): |
---|
124 | return repr(list(self.frames)) |
---|
125 | |
---|
126 | def __setitem__(self, key, value): |
---|
127 | """Set a variable in the current scope.""" |
---|
128 | self.frames[0][key] = value |
---|
129 | |
---|
130 | def _find(self, key, default=None): |
---|
131 | """Retrieve a given variable's value and the frame it was found in. |
---|
132 | |
---|
133 | Intented for internal use by directives. |
---|
134 | """ |
---|
135 | for frame in self.frames: |
---|
136 | if key in frame: |
---|
137 | return frame[key], frame |
---|
138 | return default, None |
---|
139 | |
---|
140 | def get(self, key, default=None): |
---|
141 | """Get a variable's value, starting at the current scope and going |
---|
142 | upward. |
---|
143 | """ |
---|
144 | for frame in self.frames: |
---|
145 | if key in frame: |
---|
146 | return frame[key] |
---|
147 | return default |
---|
148 | __getitem__ = get |
---|
149 | |
---|
150 | def push(self, data): |
---|
151 | """Push a new scope on the stack.""" |
---|
152 | |
---|
153 | def pop(self): |
---|
154 | """Pop the top-most scope from the stack.""" |
---|
155 | |
---|
156 | |
---|
157 | class Directive(object): |
---|
158 | """Abstract base class for template directives. |
---|
159 | |
---|
160 | A directive is basically a callable that takes three positional arguments: |
---|
161 | `ctxt` is the template data context, `stream` is an iterable over the |
---|
162 | events that the directive applies to, and `directives` is is a list of |
---|
163 | other directives on the same stream that need to be applied. |
---|
164 | |
---|
165 | Directives can be "anonymous" or "registered". Registered directives can be |
---|
166 | applied by the template author using an XML attribute with the |
---|
167 | corresponding name in the template. Such directives should be subclasses of |
---|
168 | this base class that can be instantiated with the value of the directive |
---|
169 | attribute as parameter. |
---|
170 | |
---|
171 | Anonymous directives are simply functions conforming to the protocol |
---|
172 | described above, and can only be applied programmatically (for example by |
---|
173 | template filters). |
---|
174 | """ |
---|
175 | __slots__ = ['expr'] |
---|
176 | |
---|
177 | def __init__(self, value, namespaces=None, filename=None, lineno=-1, |
---|
178 | offset=-1): |
---|
179 | try: |
---|
180 | self.expr = value and Expression(value, filename, lineno) or None |
---|
181 | except SyntaxError, err: |
---|
182 | err.msg += ' in expression "%s" of "%s" directive' % (value, |
---|
183 | self.tagname) |
---|
184 | raise TemplateSyntaxError(err, filename, lineno, |
---|
185 | offset + (err.offset or 0)) |
---|
186 | |
---|
187 | def __call__(self, stream, ctxt, directives): |
---|
188 | raise NotImplementedError |
---|
189 | |
---|
190 | def __repr__(self): |
---|
191 | expr = '' |
---|
192 | if self.expr is not None: |
---|
193 | expr = ' "%s"' % self.expr.source |
---|
194 | return '<%s%s>' % (self.__class__.__name__, expr) |
---|
195 | |
---|
196 | def tagname(self): |
---|
197 | """Return the local tag name of the directive as it is used in |
---|
198 | templates. |
---|
199 | """ |
---|
200 | return self.__class__.__name__.lower().replace('directive', '') |
---|
201 | tagname = property(tagname) |
---|
202 | |
---|
203 | |
---|
204 | def _apply_directives(stream, ctxt, directives): |
---|
205 | """Apply the given directives to the stream.""" |
---|
206 | if directives: |
---|
207 | stream = directives[0](iter(stream), ctxt, directives[1:]) |
---|
208 | return stream |
---|
209 | |
---|
210 | def _assignment(ast): |
---|
211 | """Takes the AST representation of an assignment, and returns a function |
---|
212 | that applies the assignment of a given value to a dictionary. |
---|
213 | """ |
---|
214 | def _names(node): |
---|
215 | if isinstance(node, (compiler.ast.AssTuple, compiler.ast.Tuple)): |
---|
216 | return tuple([_names(child) for child in node.nodes]) |
---|
217 | elif isinstance(node, (compiler.ast.AssName, compiler.ast.Name)): |
---|
218 | return node.name |
---|
219 | def _assign(data, value, names=_names(ast)): |
---|
220 | if type(names) is tuple: |
---|
221 | for idx in range(len(names)): |
---|
222 | _assign(data, value[idx], names[idx]) |
---|
223 | else: |
---|
224 | data[names] = value |
---|
225 | return _assign |
---|
226 | |
---|
227 | |
---|
228 | class AttrsDirective(Directive): |
---|
229 | """Implementation of the `py:attrs` template directive. |
---|
230 | |
---|
231 | The value of the `py:attrs` attribute should be a dictionary or a sequence |
---|
232 | of `(name, value)` tuples. The items in that dictionary or sequence are |
---|
233 | added as attributes to the element: |
---|
234 | |
---|
235 | >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> |
---|
236 | ... <li py:attrs="foo">Bar</li> |
---|
237 | ... </ul>''') |
---|
238 | >>> print tmpl.generate(foo={'class': 'collapse'}) |
---|
239 | <ul> |
---|
240 | <li class="collapse">Bar</li> |
---|
241 | </ul> |
---|
242 | >>> print tmpl.generate(foo=[('class', 'collapse')]) |
---|
243 | <ul> |
---|
244 | <li class="collapse">Bar</li> |
---|
245 | </ul> |
---|
246 | |
---|
247 | If the value evaluates to `None` (or any other non-truth value), no |
---|
248 | attributes are added: |
---|
249 | |
---|
250 | >>> print tmpl.generate(foo=None) |
---|
251 | <ul> |
---|
252 | <li>Bar</li> |
---|
253 | </ul> |
---|
254 | """ |
---|
255 | __slots__ = [] |
---|
256 | |
---|
257 | def __call__(self, stream, ctxt, directives): |
---|
258 | def _generate(): |
---|
259 | kind, (tag, attrib), pos = stream.next() |
---|
260 | attrs = self.expr.evaluate(ctxt) |
---|
261 | if attrs: |
---|
262 | attrib = Attrs(attrib[:]) |
---|
263 | if isinstance(attrs, Stream): |
---|
264 | try: |
---|
265 | attrs = iter(attrs).next() |
---|
266 | except StopIteration: |
---|
267 | attrs = [] |
---|
268 | elif not isinstance(attrs, list): # assume it's a dict |
---|
269 | attrs = attrs.items() |
---|
270 | for name, value in attrs: |
---|
271 | if value is None: |
---|
272 | attrib.remove(name) |
---|
273 | else: |
---|
274 | attrib.set(name, unicode(value).strip()) |
---|
275 | yield kind, (tag, attrib), pos |
---|
276 | for event in stream: |
---|
277 | yield event |
---|
278 | |
---|
279 | return _apply_directives(_generate(), ctxt, directives) |
---|
280 | |
---|
281 | |
---|
282 | class ContentDirective(Directive): |
---|
283 | """Implementation of the `py:content` template directive. |
---|
284 | |
---|
285 | This directive replaces the content of the element with the result of |
---|
286 | evaluating the value of the `py:content` attribute: |
---|
287 | |
---|
288 | >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> |
---|
289 | ... <li py:content="bar">Hello</li> |
---|
290 | ... </ul>''') |
---|
291 | >>> print tmpl.generate(bar='Bye') |
---|
292 | <ul> |
---|
293 | <li>Bye</li> |
---|
294 | </ul> |
---|
295 | """ |
---|
296 | __slots__ = [] |
---|
297 | |
---|
298 | def __call__(self, stream, ctxt, directives): |
---|
299 | def _generate(): |
---|
300 | kind, data, pos = stream.next() |
---|
301 | if kind is START: |
---|
302 | yield kind, data, pos # emit start tag |
---|
303 | yield EXPR, self.expr, pos |
---|
304 | previous = stream.next() |
---|
305 | for event in stream: |
---|
306 | previous = event |
---|
307 | if previous is not None: |
---|
308 | yield previous |
---|
309 | |
---|
310 | return _apply_directives(_generate(), ctxt, directives) |
---|
311 | |
---|
312 | |
---|
313 | class DefDirective(Directive): |
---|
314 | """Implementation of the `py:def` template directive. |
---|
315 | |
---|
316 | This directive can be used to create "Named Template Functions", which |
---|
317 | are template snippets that are not actually output during normal |
---|
318 | processing, but rather can be expanded from expressions in other places |
---|
319 | in the template. |
---|
320 | |
---|
321 | A named template function can be used just like a normal Python function |
---|
322 | from template expressions: |
---|
323 | |
---|
324 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
325 | ... <p py:def="echo(greeting, name='world')" class="message"> |
---|
326 | ... ${greeting}, ${name}! |
---|
327 | ... </p> |
---|
328 | ... ${echo('Hi', name='you')} |
---|
329 | ... </div>''') |
---|
330 | >>> print tmpl.generate(bar='Bye') |
---|
331 | <div> |
---|
332 | <p class="message"> |
---|
333 | Hi, you! |
---|
334 | </p> |
---|
335 | </div> |
---|
336 | |
---|
337 | If a function does not require parameters, the parenthesis can be omitted |
---|
338 | both when defining and when calling it: |
---|
339 | |
---|
340 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
341 | ... <p py:def="helloworld" class="message"> |
---|
342 | ... Hello, world! |
---|
343 | ... </p> |
---|
344 | ... ${helloworld} |
---|
345 | ... </div>''') |
---|
346 | >>> print tmpl.generate(bar='Bye') |
---|
347 | <div> |
---|
348 | <p class="message"> |
---|
349 | Hello, world! |
---|
350 | </p> |
---|
351 | </div> |
---|
352 | """ |
---|
353 | __slots__ = ['name', 'args', 'defaults'] |
---|
354 | |
---|
355 | ATTRIBUTE = 'function' |
---|
356 | |
---|
357 | def __init__(self, args, namespaces=None, filename=None, lineno=-1, |
---|
358 | offset=-1): |
---|
359 | Directive.__init__(self, None, namespaces, filename, lineno, offset) |
---|
360 | ast = _parse(args).node |
---|
361 | self.args = [] |
---|
362 | self.defaults = {} |
---|
363 | if isinstance(ast, compiler.ast.CallFunc): |
---|
364 | self.name = ast.node.name |
---|
365 | for arg in ast.args: |
---|
366 | if isinstance(arg, compiler.ast.Keyword): |
---|
367 | self.args.append(arg.name) |
---|
368 | self.defaults[arg.name] = Expression(arg.expr, filename, |
---|
369 | lineno) |
---|
370 | else: |
---|
371 | self.args.append(arg.name) |
---|
372 | else: |
---|
373 | self.name = ast.name |
---|
374 | |
---|
375 | def __call__(self, stream, ctxt, directives): |
---|
376 | stream = list(stream) |
---|
377 | |
---|
378 | def function(*args, **kwargs): |
---|
379 | scope = {} |
---|
380 | args = list(args) # make mutable |
---|
381 | for name in self.args: |
---|
382 | if args: |
---|
383 | scope[name] = args.pop(0) |
---|
384 | else: |
---|
385 | if name in kwargs: |
---|
386 | val = kwargs.pop(name) |
---|
387 | else: |
---|
388 | val = self.defaults.get(name).evaluate(ctxt) |
---|
389 | scope[name] = val |
---|
390 | ctxt.push(scope) |
---|
391 | for event in _apply_directives(stream, ctxt, directives): |
---|
392 | yield event |
---|
393 | ctxt.pop() |
---|
394 | try: |
---|
395 | function.__name__ = self.name |
---|
396 | except TypeError: |
---|
397 | # Function name can't be set in Python 2.3 |
---|
398 | pass |
---|
399 | |
---|
400 | # Store the function reference in the bottom context frame so that it |
---|
401 | # doesn't get popped off before processing the template has finished |
---|
402 | # FIXME: this makes context data mutable as a side-effect |
---|
403 | ctxt.frames[-1][self.name] = function |
---|
404 | |
---|
405 | return [] |
---|
406 | |
---|
407 | def __repr__(self): |
---|
408 | return '<%s "%s">' % (self.__class__.__name__, self.name) |
---|
409 | |
---|
410 | |
---|
411 | class ForDirective(Directive): |
---|
412 | """Implementation of the `py:for` template directive for repeating an |
---|
413 | element based on an iterable in the context data. |
---|
414 | |
---|
415 | >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> |
---|
416 | ... <li py:for="item in items">${item}</li> |
---|
417 | ... </ul>''') |
---|
418 | >>> print tmpl.generate(items=[1, 2, 3]) |
---|
419 | <ul> |
---|
420 | <li>1</li><li>2</li><li>3</li> |
---|
421 | </ul> |
---|
422 | """ |
---|
423 | __slots__ = ['assign', 'filename'] |
---|
424 | |
---|
425 | ATTRIBUTE = 'each' |
---|
426 | |
---|
427 | def __init__(self, value, namespaces=None, filename=None, lineno=-1, |
---|
428 | offset=-1): |
---|
429 | if ' in ' not in value: |
---|
430 | raise TemplateSyntaxError('"in" keyword missing in "for" directive', |
---|
431 | filename, lineno, offset) |
---|
432 | assign, value = value.split(' in ', 1) |
---|
433 | ast = _parse(assign, 'exec') |
---|
434 | self.assign = _assignment(ast.node.nodes[0].expr) |
---|
435 | self.filename = filename |
---|
436 | Directive.__init__(self, value.strip(), namespaces, filename, lineno, |
---|
437 | offset) |
---|
438 | |
---|
439 | def __call__(self, stream, ctxt, directives): |
---|
440 | iterable = self.expr.evaluate(ctxt) |
---|
441 | if iterable is None: |
---|
442 | return |
---|
443 | |
---|
444 | assign = self.assign |
---|
445 | scope = {} |
---|
446 | stream = list(stream) |
---|
447 | try: |
---|
448 | iterator = iter(iterable) |
---|
449 | for item in iterator: |
---|
450 | assign(scope, item) |
---|
451 | ctxt.push(scope) |
---|
452 | for event in _apply_directives(stream, ctxt, directives): |
---|
453 | yield event |
---|
454 | ctxt.pop() |
---|
455 | except TypeError, e: |
---|
456 | raise TemplateRuntimeError(str(e), self.filename, *stream[0][2][1:]) |
---|
457 | |
---|
458 | def __repr__(self): |
---|
459 | return '<%s>' % self.__class__.__name__ |
---|
460 | |
---|
461 | |
---|
462 | class IfDirective(Directive): |
---|
463 | """Implementation of the `py:if` template directive for conditionally |
---|
464 | excluding elements from being output. |
---|
465 | |
---|
466 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
467 | ... <b py:if="foo">${bar}</b> |
---|
468 | ... </div>''') |
---|
469 | >>> print tmpl.generate(foo=True, bar='Hello') |
---|
470 | <div> |
---|
471 | <b>Hello</b> |
---|
472 | </div> |
---|
473 | """ |
---|
474 | __slots__ = [] |
---|
475 | |
---|
476 | ATTRIBUTE = 'test' |
---|
477 | |
---|
478 | def __call__(self, stream, ctxt, directives): |
---|
479 | if self.expr.evaluate(ctxt): |
---|
480 | return _apply_directives(stream, ctxt, directives) |
---|
481 | return [] |
---|
482 | |
---|
483 | |
---|
484 | class MatchDirective(Directive): |
---|
485 | """Implementation of the `py:match` template directive. |
---|
486 | |
---|
487 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
488 | ... <span py:match="greeting"> |
---|
489 | ... Hello ${select('@name')} |
---|
490 | ... </span> |
---|
491 | ... <greeting name="Dude" /> |
---|
492 | ... </div>''') |
---|
493 | >>> print tmpl.generate() |
---|
494 | <div> |
---|
495 | <span> |
---|
496 | Hello Dude |
---|
497 | </span> |
---|
498 | </div> |
---|
499 | """ |
---|
500 | __slots__ = ['path', 'namespaces'] |
---|
501 | |
---|
502 | ATTRIBUTE = 'path' |
---|
503 | |
---|
504 | def __init__(self, value, namespaces=None, filename=None, lineno=-1, |
---|
505 | offset=-1): |
---|
506 | Directive.__init__(self, None, namespaces, filename, lineno, offset) |
---|
507 | self.path = Path(value, filename, lineno) |
---|
508 | if namespaces is None: |
---|
509 | namespaces = {} |
---|
510 | self.namespaces = namespaces.copy() |
---|
511 | |
---|
512 | def __call__(self, stream, ctxt, directives): |
---|
513 | ctxt._match_templates.append((self.path.test(ignore_context=True), |
---|
514 | self.path, list(stream), self.namespaces, |
---|
515 | directives)) |
---|
516 | return [] |
---|
517 | |
---|
518 | def __repr__(self): |
---|
519 | return '<%s "%s">' % (self.__class__.__name__, self.path.source) |
---|
520 | |
---|
521 | |
---|
522 | class ReplaceDirective(Directive): |
---|
523 | """Implementation of the `py:replace` template directive. |
---|
524 | |
---|
525 | This directive replaces the element with the result of evaluating the |
---|
526 | value of the `py:replace` attribute: |
---|
527 | |
---|
528 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
529 | ... <span py:replace="bar">Hello</span> |
---|
530 | ... </div>''') |
---|
531 | >>> print tmpl.generate(bar='Bye') |
---|
532 | <div> |
---|
533 | Bye |
---|
534 | </div> |
---|
535 | |
---|
536 | This directive is equivalent to `py:content` combined with `py:strip`, |
---|
537 | providing a less verbose way to achieve the same effect: |
---|
538 | |
---|
539 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
540 | ... <span py:content="bar" py:strip="">Hello</span> |
---|
541 | ... </div>''') |
---|
542 | >>> print tmpl.generate(bar='Bye') |
---|
543 | <div> |
---|
544 | Bye |
---|
545 | </div> |
---|
546 | """ |
---|
547 | __slots__ = [] |
---|
548 | |
---|
549 | def __call__(self, stream, ctxt, directives): |
---|
550 | kind, data, pos = stream.next() |
---|
551 | yield EXPR, self.expr, pos |
---|
552 | |
---|
553 | |
---|
554 | class StripDirective(Directive): |
---|
555 | """Implementation of the `py:strip` template directive. |
---|
556 | |
---|
557 | When the value of the `py:strip` attribute evaluates to `True`, the element |
---|
558 | is stripped from the output |
---|
559 | |
---|
560 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
561 | ... <div py:strip="True"><b>foo</b></div> |
---|
562 | ... </div>''') |
---|
563 | >>> print tmpl.generate() |
---|
564 | <div> |
---|
565 | <b>foo</b> |
---|
566 | </div> |
---|
567 | |
---|
568 | Leaving the attribute value empty is equivalent to a truth value. |
---|
569 | |
---|
570 | This directive is particulary interesting for named template functions or |
---|
571 | match templates that do not generate a top-level element: |
---|
572 | |
---|
573 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
574 | ... <div py:def="echo(what)" py:strip=""> |
---|
575 | ... <b>${what}</b> |
---|
576 | ... </div> |
---|
577 | ... ${echo('foo')} |
---|
578 | ... </div>''') |
---|
579 | >>> print tmpl.generate() |
---|
580 | <div> |
---|
581 | <b>foo</b> |
---|
582 | </div> |
---|
583 | """ |
---|
584 | __slots__ = [] |
---|
585 | |
---|
586 | def __call__(self, stream, ctxt, directives): |
---|
587 | def _generate(): |
---|
588 | if self.expr: |
---|
589 | strip = self.expr.evaluate(ctxt) |
---|
590 | else: |
---|
591 | strip = True |
---|
592 | if strip: |
---|
593 | stream.next() # skip start tag |
---|
594 | previous = stream.next() |
---|
595 | for event in stream: |
---|
596 | yield previous |
---|
597 | previous = event |
---|
598 | else: |
---|
599 | for event in stream: |
---|
600 | yield event |
---|
601 | |
---|
602 | return _apply_directives(_generate(), ctxt, directives) |
---|
603 | |
---|
604 | |
---|
605 | class ChooseDirective(Directive): |
---|
606 | """Implementation of the `py:choose` directive for conditionally selecting |
---|
607 | one of several body elements to display. |
---|
608 | |
---|
609 | If the `py:choose` expression is empty the expressions of nested `py:when` |
---|
610 | directives are tested for truth. The first true `py:when` body is output. |
---|
611 | If no `py:when` directive is matched then the fallback directive |
---|
612 | `py:otherwise` will be used. |
---|
613 | |
---|
614 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" |
---|
615 | ... py:choose=""> |
---|
616 | ... <span py:when="0 == 1">0</span> |
---|
617 | ... <span py:when="1 == 1">1</span> |
---|
618 | ... <span py:otherwise="">2</span> |
---|
619 | ... </div>''') |
---|
620 | >>> print tmpl.generate() |
---|
621 | <div> |
---|
622 | <span>1</span> |
---|
623 | </div> |
---|
624 | |
---|
625 | If the `py:choose` directive contains an expression, the nested `py:when` |
---|
626 | directives are tested for equality to the `py:choose` expression: |
---|
627 | |
---|
628 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" |
---|
629 | ... py:choose="2"> |
---|
630 | ... <span py:when="1">1</span> |
---|
631 | ... <span py:when="2">2</span> |
---|
632 | ... </div>''') |
---|
633 | >>> print tmpl.generate() |
---|
634 | <div> |
---|
635 | <span>2</span> |
---|
636 | </div> |
---|
637 | |
---|
638 | Behavior is undefined if a `py:choose` block contains content outside a |
---|
639 | `py:when` or `py:otherwise` block. Behavior is also undefined if a |
---|
640 | `py:otherwise` occurs before `py:when` blocks. |
---|
641 | """ |
---|
642 | __slots__ = ['matched', 'value'] |
---|
643 | |
---|
644 | ATTRIBUTE = 'test' |
---|
645 | |
---|
646 | def __call__(self, stream, ctxt, directives): |
---|
647 | frame = dict({'_choose.matched': False}) |
---|
648 | if self.expr: |
---|
649 | frame['_choose.value'] = self.expr.evaluate(ctxt) |
---|
650 | ctxt.push(frame) |
---|
651 | for event in _apply_directives(stream, ctxt, directives): |
---|
652 | yield event |
---|
653 | ctxt.pop() |
---|
654 | |
---|
655 | |
---|
656 | class WhenDirective(Directive): |
---|
657 | """Implementation of the `py:when` directive for nesting in a parent with |
---|
658 | the `py:choose` directive. |
---|
659 | |
---|
660 | See the documentation of `py:choose` for usage. |
---|
661 | """ |
---|
662 | __slots__ = ['filename'] |
---|
663 | |
---|
664 | ATTRIBUTE = 'test' |
---|
665 | |
---|
666 | def __init__(self, value, namespaces=None, filename=None, lineno=-1, |
---|
667 | offset=-1): |
---|
668 | Directive.__init__(self, value, namespaces, filename, lineno, offset) |
---|
669 | self.filename = filename |
---|
670 | |
---|
671 | def __call__(self, stream, ctxt, directives): |
---|
672 | matched, frame = ctxt._find('_choose.matched') |
---|
673 | if not frame: |
---|
674 | raise TemplateRuntimeError('"when" directives can only be used ' |
---|
675 | 'inside a "choose" directive', |
---|
676 | self.filename, *stream.next()[2][1:]) |
---|
677 | if matched: |
---|
678 | return [] |
---|
679 | if not self.expr: |
---|
680 | raise TemplateRuntimeError('"when" directive has no test condition', |
---|
681 | self.filename, *stream.next()[2][1:]) |
---|
682 | value = self.expr.evaluate(ctxt) |
---|
683 | if '_choose.value' in frame: |
---|
684 | matched = (value == frame['_choose.value']) |
---|
685 | else: |
---|
686 | matched = bool(value) |
---|
687 | frame['_choose.matched'] = matched |
---|
688 | if not matched: |
---|
689 | return [] |
---|
690 | |
---|
691 | return _apply_directives(stream, ctxt, directives) |
---|
692 | |
---|
693 | |
---|
694 | class OtherwiseDirective(Directive): |
---|
695 | """Implementation of the `py:otherwise` directive for nesting in a parent |
---|
696 | with the `py:choose` directive. |
---|
697 | |
---|
698 | See the documentation of `py:choose` for usage. |
---|
699 | """ |
---|
700 | __slots__ = ['filename'] |
---|
701 | |
---|
702 | def __init__(self, value, namespaces=None, filename=None, lineno=-1, |
---|
703 | offset=-1): |
---|
704 | Directive.__init__(self, None, namespaces, filename, lineno, offset) |
---|
705 | self.filename = filename |
---|
706 | |
---|
707 | def __call__(self, stream, ctxt, directives): |
---|
708 | matched, frame = ctxt._find('_choose.matched') |
---|
709 | if not frame: |
---|
710 | raise TemplateRuntimeError('an "otherwise" directive can only be ' |
---|
711 | 'used inside a "choose" directive', |
---|
712 | self.filename, *stream.next()[2][1:]) |
---|
713 | if matched: |
---|
714 | return [] |
---|
715 | frame['_choose.matched'] = True |
---|
716 | |
---|
717 | return _apply_directives(stream, ctxt, directives) |
---|
718 | |
---|
719 | |
---|
720 | class WithDirective(Directive): |
---|
721 | """Implementation of the `py:with` template directive, which allows |
---|
722 | shorthand access to variables and expressions. |
---|
723 | |
---|
724 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
---|
725 | ... <span py:with="y=7; z=x+10">$x $y $z</span> |
---|
726 | ... </div>''') |
---|
727 | >>> print tmpl.generate(x=42) |
---|
728 | <div> |
---|
729 | <span>42 7 52</span> |
---|
730 | </div> |
---|
731 | """ |
---|
732 | __slots__ = ['vars'] |
---|
733 | |
---|
734 | ATTRIBUTE = 'vars' |
---|
735 | |
---|
736 | def __init__(self, value, namespaces=None, filename=None, lineno=-1, |
---|
737 | offset=-1): |
---|
738 | Directive.__init__(self, None, namespaces, filename, lineno, offset) |
---|
739 | self.vars = [] |
---|
740 | value = value.strip() |
---|
741 | try: |
---|
742 | ast = _parse(value, 'exec').node |
---|
743 | for node in ast.nodes: |
---|
744 | if isinstance(node, compiler.ast.Discard): |
---|
745 | continue |
---|
746 | elif not isinstance(node, compiler.ast.Assign): |
---|
747 | raise TemplateSyntaxError('only assignment allowed in ' |
---|
748 | 'value of the "with" directive', |
---|
749 | filename, lineno, offset) |
---|
750 | self.vars.append(([_assignment(n) for n in node.nodes], |
---|
751 | Expression(node.expr, filename, lineno))) |
---|
752 | except SyntaxError, err: |
---|
753 | err.msg += ' in expression "%s" of "%s" directive' % (value, |
---|
754 | self.tagname) |
---|
755 | raise TemplateSyntaxError(err, filename, lineno, |
---|
756 | offset + (err.offset or 0)) |
---|
757 | |
---|
758 | def __call__(self, stream, ctxt, directives): |
---|
759 | frame = {} |
---|
760 | ctxt.push(frame) |
---|
761 | for targets, expr in self.vars: |
---|
762 | value = expr.evaluate(ctxt, nocall=True) |
---|
763 | for assign in targets: |
---|
764 | assign(frame, value) |
---|
765 | for event in _apply_directives(stream, ctxt, directives): |
---|
766 | yield event |
---|
767 | ctxt.pop() |
---|
768 | |
---|
769 | def __repr__(self): |
---|
770 | return '<%s>' % (self.__class__.__name__) |
---|
771 | |
---|
772 | |
---|
773 | class TemplateMeta(type): |
---|
774 | """Meta class for templates.""" |
---|
775 | |
---|
776 | def __new__(cls, name, bases, d): |
---|
777 | if 'directives' in d: |
---|
778 | d['_dir_by_name'] = dict(d['directives']) |
---|
779 | d['_dir_order'] = [directive[1] for directive in d['directives']] |
---|
780 | |
---|
781 | return type.__new__(cls, name, bases, d) |
---|
782 | |
---|
783 | |
---|
784 | class Template(object): |
---|
785 | """Abstract template base class. |
---|
786 | |
---|
787 | This class implements most of the template processing model, but does not |
---|
788 | specify the syntax of templates. |
---|
789 | """ |
---|
790 | __metaclass__ = TemplateMeta |
---|
791 | |
---|
792 | EXPR = StreamEventKind('EXPR') # an expression |
---|
793 | SUB = StreamEventKind('SUB') # a "subprogram" |
---|
794 | |
---|
795 | def __init__(self, source, basedir=None, filename=None, loader=None, |
---|
796 | encoding=None): |
---|
797 | """Initialize a template from either a string or a file-like object.""" |
---|
798 | if isinstance(source, basestring): |
---|
799 | self.source = StringIO(source) |
---|
800 | else: |
---|
801 | self.source = source |
---|
802 | self.basedir = basedir |
---|
803 | self.filename = filename |
---|
804 | if basedir and filename: |
---|
805 | self.filepath = os.path.join(basedir, filename) |
---|
806 | else: |
---|
807 | self.filepath = filename |
---|
808 | |
---|
809 | self.filters = [self._flatten, self._eval] |
---|
810 | |
---|
811 | self.stream = self._parse(encoding) |
---|
812 | |
---|
813 | def __repr__(self): |
---|
814 | return '<%s "%s">' % (self.__class__.__name__, self.filename) |
---|
815 | |
---|
816 | def _parse(self, encoding): |
---|
817 | """Parse the template. |
---|
818 | |
---|
819 | The parsing stage parses the template and constructs a list of |
---|
820 | directives that will be executed in the render stage. The input is |
---|
821 | split up into literal output (text that does not depend on the context |
---|
822 | data) and directives or expressions. |
---|
823 | """ |
---|
824 | raise NotImplementedError |
---|
825 | |
---|
826 | _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}', re.DOTALL) |
---|
827 | _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z_][a-zA-Z0-9_\.]*)') |
---|
828 | |
---|
829 | def _interpolate(cls, text, basedir=None, filename=None, lineno=-1, |
---|
830 | offset=0): |
---|
831 | """Parse the given string and extract expressions. |
---|
832 | |
---|
833 | This method returns a list containing both literal text and `Expression` |
---|
834 | objects. |
---|
835 | |
---|
836 | @param text: the text to parse |
---|
837 | @param lineno: the line number at which the text was found (optional) |
---|
838 | @param offset: the column number at which the text starts in the source |
---|
839 | (optional) |
---|
840 | """ |
---|
841 | filepath = filename |
---|
842 | if filepath and basedir: |
---|
843 | filepath = os.path.join(basedir, filepath) |
---|
844 | def _interpolate(text, patterns, lineno=lineno, offset=offset): |
---|
845 | for idx, grp in enumerate(patterns.pop(0).split(text)): |
---|
846 | if idx % 2: |
---|
847 | try: |
---|
848 | yield EXPR, Expression(grp.strip(), filepath, lineno), \ |
---|
849 | (filename, lineno, offset) |
---|
850 | except SyntaxError, err: |
---|
851 | raise TemplateSyntaxError(err, filepath, lineno, |
---|
852 | offset + (err.offset or 0)) |
---|
853 | elif grp: |
---|
854 | if patterns: |
---|
855 | for result in _interpolate(grp, patterns[:]): |
---|
856 | yield result |
---|
857 | else: |
---|
858 | yield TEXT, grp.replace('$$', '$'), \ |
---|
859 | (filename, lineno, offset) |
---|
860 | if '\n' in grp: |
---|
861 | lines = grp.splitlines() |
---|
862 | lineno += len(lines) - 1 |
---|
863 | offset += len(lines[-1]) |
---|
864 | else: |
---|
865 | offset += len(grp) |
---|
866 | return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE]) |
---|
867 | _interpolate = classmethod(_interpolate) |
---|
868 | |
---|
869 | def generate(self, *args, **kwargs): |
---|
870 | """Apply the template to the given context data. |
---|
871 | |
---|
872 | Any keyword arguments are made available to the template as context |
---|
873 | data. |
---|
874 | |
---|
875 | Only one positional argument is accepted: if it is provided, it must be |
---|
876 | an instance of the `Context` class, and keyword arguments are ignored. |
---|
877 | This calling style is used for internal processing. |
---|
878 | |
---|
879 | @return: a markup event stream representing the result of applying |
---|
880 | the template to the context data. |
---|
881 | """ |
---|
882 | if args: |
---|
883 | assert len(args) == 1 |
---|
884 | ctxt = args[0] |
---|
885 | if ctxt is None: |
---|
886 | ctxt = Context(**kwargs) |
---|
887 | assert isinstance(ctxt, Context) |
---|
888 | else: |
---|
889 | ctxt = Context(**kwargs) |
---|
890 | |
---|
891 | stream = self.stream |
---|
892 | for filter_ in self.filters: |
---|
893 | stream = filter_(iter(stream), ctxt) |
---|
894 | return Stream(stream) |
---|
895 | |
---|
896 | def _eval(self, stream, ctxt): |
---|
897 | """Internal stream filter that evaluates any expressions in `START` and |
---|
898 | `TEXT` events. |
---|
899 | """ |
---|
900 | filters = (self._flatten, self._eval) |
---|
901 | |
---|
902 | for kind, data, pos in stream: |
---|
903 | |
---|
904 | if kind is START and data[1]: |
---|
905 | # Attributes may still contain expressions in start tags at |
---|
906 | # this point, so do some evaluation |
---|
907 | tag, attrib = data |
---|
908 | new_attrib = [] |
---|
909 | for name, substream in attrib: |
---|
910 | if isinstance(substream, basestring): |
---|
911 | value = substream |
---|
912 | else: |
---|
913 | values = [] |
---|
914 | for subkind, subdata, subpos in self._eval(substream, |
---|
915 | ctxt): |
---|
916 | if subkind is TEXT: |
---|
917 | values.append(subdata) |
---|
918 | value = [x for x in values if x is not None] |
---|
919 | if not value: |
---|
920 | continue |
---|
921 | new_attrib.append((name, u''.join(value))) |
---|
922 | yield kind, (tag, Attrs(new_attrib)), pos |
---|
923 | |
---|
924 | elif kind is EXPR: |
---|
925 | result = data.evaluate(ctxt) |
---|
926 | if result is None: |
---|
927 | continue |
---|
928 | |
---|
929 | # First check for a string, otherwise the iterable test below |
---|
930 | # succeeds, and the string will be chopped up into individual |
---|
931 | # characters |
---|
932 | if isinstance(result, basestring): |
---|
933 | yield TEXT, result, pos |
---|
934 | else: |
---|
935 | # Test if the expression evaluated to an iterable, in which |
---|
936 | # case we yield the individual items |
---|
937 | try: |
---|
938 | substream = _ensure(iter(result)) |
---|
939 | except TypeError: |
---|
940 | # Neither a string nor an iterable, so just pass it |
---|
941 | # through |
---|
942 | yield TEXT, unicode(result), pos |
---|
943 | else: |
---|
944 | for filter_ in filters: |
---|
945 | substream = filter_(substream, ctxt) |
---|
946 | for event in substream: |
---|
947 | yield event |
---|
948 | |
---|
949 | else: |
---|
950 | yield kind, data, pos |
---|
951 | |
---|
952 | def _flatten(self, stream, ctxt): |
---|
953 | """Internal stream filter that expands `SUB` events in the stream.""" |
---|
954 | for kind, data, pos in stream: |
---|
955 | if kind is SUB: |
---|
956 | # This event is a list of directives and a list of nested |
---|
957 | # events to which those directives should be applied |
---|
958 | directives, substream = data |
---|
959 | substream = _apply_directives(substream, ctxt, directives) |
---|
960 | for event in self._flatten(substream, ctxt): |
---|
961 | yield event |
---|
962 | else: |
---|
963 | yield kind, data, pos |
---|
964 | |
---|
965 | |
---|
966 | EXPR = Template.EXPR |
---|
967 | SUB = Template.SUB |
---|
968 | |
---|
969 | |
---|
970 | class MarkupTemplate(Template): |
---|
971 | """Implementation of the template language for XML-based templates. |
---|
972 | |
---|
973 | >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> |
---|
974 | ... <li py:for="item in items">${item}</li> |
---|
975 | ... </ul>''') |
---|
976 | >>> print tmpl.generate(items=[1, 2, 3]) |
---|
977 | <ul> |
---|
978 | <li>1</li><li>2</li><li>3</li> |
---|
979 | </ul> |
---|
980 | """ |
---|
981 | NAMESPACE = Namespace('http://genshi.edgewall.org/') |
---|
982 | |
---|
983 | directives = [('def', DefDirective), |
---|
984 | ('match', MatchDirective), |
---|
985 | ('when', WhenDirective), |
---|
986 | ('otherwise', OtherwiseDirective), |
---|
987 | ('for', ForDirective), |
---|
988 | ('if', IfDirective), |
---|
989 | ('choose', ChooseDirective), |
---|
990 | ('with', WithDirective), |
---|
991 | ('replace', ReplaceDirective), |
---|
992 | ('content', ContentDirective), |
---|
993 | ('attrs', AttrsDirective), |
---|
994 | ('strip', StripDirective)] |
---|
995 | |
---|
996 | def __init__(self, source, basedir=None, filename=None, loader=None, |
---|
997 | encoding=None): |
---|
998 | """Initialize a template from either a string or a file-like object.""" |
---|
999 | Template.__init__(self, source, basedir=basedir, filename=filename, |
---|
1000 | loader=loader, encoding=encoding) |
---|
1001 | |
---|
1002 | self.filters.append(self._match) |
---|
1003 | if loader: |
---|
1004 | from genshi.filters import IncludeFilter |
---|
1005 | self.filters.append(IncludeFilter(loader)) |
---|
1006 | |
---|
1007 | def _parse(self, encoding): |
---|
1008 | """Parse the template from an XML document.""" |
---|
1009 | stream = [] # list of events of the "compiled" template |
---|
1010 | dirmap = {} # temporary mapping of directives to elements |
---|
1011 | ns_prefix = {} |
---|
1012 | depth = 0 |
---|
1013 | |
---|
1014 | for kind, data, pos in XMLParser(self.source, filename=self.filename, |
---|
1015 | encoding=encoding): |
---|
1016 | |
---|
1017 | if kind is START_NS: |
---|
1018 | # Strip out the namespace declaration for template directives |
---|
1019 | prefix, uri = data |
---|
1020 | ns_prefix[prefix] = uri |
---|
1021 | if uri != self.NAMESPACE: |
---|
1022 | stream.append((kind, data, pos)) |
---|
1023 | |
---|
1024 | elif kind is END_NS: |
---|
1025 | uri = ns_prefix.pop(data, None) |
---|
1026 | if uri and uri != self.NAMESPACE: |
---|
1027 | stream.append((kind, data, pos)) |
---|
1028 | |
---|
1029 | elif kind is START: |
---|
1030 | # Record any directive attributes in start tags |
---|
1031 | tag, attrib = data |
---|
1032 | directives = [] |
---|
1033 | strip = False |
---|
1034 | |
---|
1035 | if tag in self.NAMESPACE: |
---|
1036 | cls = self._dir_by_name.get(tag.localname) |
---|
1037 | if cls is None: |
---|
1038 | raise BadDirectiveError(tag.localname, self.filepath, |
---|
1039 | pos[1]) |
---|
1040 | value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '') |
---|
1041 | directives.append(cls(value, ns_prefix, self.filepath, |
---|
1042 | pos[1], pos[2])) |
---|
1043 | strip = True |
---|
1044 | |
---|
1045 | new_attrib = [] |
---|
1046 | for name, value in attrib: |
---|
1047 | if name in self.NAMESPACE: |
---|
1048 | cls = self._dir_by_name.get(name.localname) |
---|
1049 | if cls is None: |
---|
1050 | raise BadDirectiveError(name.localname, |
---|
1051 | self.filepath, pos[1]) |
---|
1052 | directives.append(cls(value, ns_prefix, self.filepath, |
---|
1053 | pos[1], pos[2])) |
---|
1054 | else: |
---|
1055 | if value: |
---|
1056 | value = list(self._interpolate(value, self.basedir, |
---|
1057 | *pos)) |
---|
1058 | if len(value) == 1 and value[0][0] is TEXT: |
---|
1059 | value = value[0][1] |
---|
1060 | else: |
---|
1061 | value = [(TEXT, u'', pos)] |
---|
1062 | new_attrib.append((name, value)) |
---|
1063 | |
---|
1064 | if directives: |
---|
1065 | index = self._dir_order.index |
---|
1066 | directives.sort(lambda a, b: cmp(index(a.__class__), |
---|
1067 | index(b.__class__))) |
---|
1068 | dirmap[(depth, tag)] = (directives, len(stream), strip) |
---|
1069 | |
---|
1070 | stream.append((kind, (tag, Attrs(new_attrib)), pos)) |
---|
1071 | depth += 1 |
---|
1072 | |
---|
1073 | elif kind is END: |
---|
1074 | depth -= 1 |
---|
1075 | stream.append((kind, data, pos)) |
---|
1076 | |
---|
1077 | # If there have have directive attributes with the corresponding |
---|
1078 | # start tag, move the events inbetween into a "subprogram" |
---|
1079 | if (depth, data) in dirmap: |
---|
1080 | directives, start_offset, strip = dirmap.pop((depth, data)) |
---|
1081 | substream = stream[start_offset:] |
---|
1082 | if strip: |
---|
1083 | substream = substream[1:-1] |
---|
1084 | stream[start_offset:] = [(SUB, (directives, substream), |
---|
1085 | pos)] |
---|
1086 | |
---|
1087 | elif kind is TEXT: |
---|
1088 | for kind, data, pos in self._interpolate(data, self.basedir, |
---|
1089 | *pos): |
---|
1090 | stream.append((kind, data, pos)) |
---|
1091 | |
---|
1092 | elif kind is COMMENT: |
---|
1093 | if not data.lstrip().startswith('!'): |
---|
1094 | stream.append((kind, data, pos)) |
---|
1095 | |
---|
1096 | else: |
---|
1097 | stream.append((kind, data, pos)) |
---|
1098 | |
---|
1099 | return stream |
---|
1100 | |
---|
1101 | def _match(self, stream, ctxt, match_templates=None): |
---|
1102 | """Internal stream filter that applies any defined match templates |
---|
1103 | to the stream. |
---|
1104 | """ |
---|
1105 | if match_templates is None: |
---|
1106 | match_templates = ctxt._match_templates |
---|
1107 | |
---|
1108 | tail = [] |
---|
1109 | def _strip(stream): |
---|
1110 | depth = 1 |
---|
1111 | while 1: |
---|
1112 | kind, data, pos = stream.next() |
---|
1113 | if kind is START: |
---|
1114 | depth += 1 |
---|
1115 | elif kind is END: |
---|
1116 | depth -= 1 |
---|
1117 | if depth > 0: |
---|
1118 | yield kind, data, pos |
---|
1119 | else: |
---|
1120 | tail[:] = [(kind, data, pos)] |
---|
1121 | break |
---|
1122 | |
---|
1123 | for kind, data, pos in stream: |
---|
1124 | |
---|
1125 | # We (currently) only care about start and end events for matching |
---|
1126 | # We might care about namespace events in the future, though |
---|
1127 | if not match_templates or kind not in (START, END): |
---|
1128 | yield kind, data, pos |
---|
1129 | continue |
---|
1130 | |
---|
1131 | for idx, (test, path, template, namespaces, directives) in \ |
---|
1132 | enumerate(match_templates): |
---|
1133 | |
---|
1134 | if test(kind, data, pos, namespaces, ctxt) is True: |
---|
1135 | |
---|
1136 | # Let the remaining match templates know about the event so |
---|
1137 | # they get a chance to update their internal state |
---|
1138 | for test in [mt[0] for mt in match_templates[idx + 1:]]: |
---|
1139 | test(kind, data, pos, namespaces, ctxt) |
---|
1140 | |
---|
1141 | # Consume and store all events until an end event |
---|
1142 | # corresponding to this start event is encountered |
---|
1143 | content = chain([(kind, data, pos)], |
---|
1144 | self._match(_strip(stream), ctxt, |
---|
1145 | [match_templates[idx]]), |
---|
1146 | tail) |
---|
1147 | for filter_ in self.filters[3:]: |
---|
1148 | content = filter_(content, ctxt) |
---|
1149 | content = list(content) |
---|
1150 | |
---|
1151 | kind, data, pos = tail[0] |
---|
1152 | for test in [mt[0] for mt in match_templates]: |
---|
1153 | test(kind, data, pos, namespaces, ctxt) |
---|
1154 | |
---|
1155 | # Make the select() function available in the body of the |
---|
1156 | # match template |
---|
1157 | def select(path): |
---|
1158 | return Stream(content).select(path, namespaces, ctxt) |
---|
1159 | ctxt.push(dict(select=select)) |
---|
1160 | |
---|
1161 | # Recursively process the output |
---|
1162 | template = _apply_directives(template, ctxt, directives) |
---|
1163 | for event in self._match(self._eval(self._flatten(template, |
---|
1164 | ctxt), |
---|
1165 | ctxt), ctxt, |
---|
1166 | match_templates[:idx] + |
---|
1167 | match_templates[idx + 1:]): |
---|
1168 | yield event |
---|
1169 | |
---|
1170 | ctxt.pop() |
---|
1171 | break |
---|
1172 | |
---|
1173 | else: # no matches |
---|
1174 | yield kind, data, pos |
---|
1175 | |
---|
1176 | |
---|
1177 | class TextTemplate(Template): |
---|
1178 | """Implementation of a simple text-based template engine. |
---|
1179 | |
---|
1180 | >>> tmpl = TextTemplate('''Dear $name, |
---|
1181 | ... |
---|
1182 | ... We have the following items for you: |
---|
1183 | ... #for item in items |
---|
1184 | ... * $item |
---|
1185 | ... #end |
---|
1186 | ... |
---|
1187 | ... All the best, |
---|
1188 | ... Foobar''') |
---|
1189 | >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text') |
---|
1190 | Dear Joe, |
---|
1191 | <BLANKLINE> |
---|
1192 | We have the following items for you: |
---|
1193 | * 1 |
---|
1194 | * 2 |
---|
1195 | * 3 |
---|
1196 | <BLANKLINE> |
---|
1197 | All the best, |
---|
1198 | Foobar |
---|
1199 | """ |
---|
1200 | directives = [('def', DefDirective), |
---|
1201 | ('when', WhenDirective), |
---|
1202 | ('otherwise', OtherwiseDirective), |
---|
1203 | ('for', ForDirective), |
---|
1204 | ('if', IfDirective), |
---|
1205 | ('choose', ChooseDirective), |
---|
1206 | ('with', WithDirective)] |
---|
1207 | |
---|
1208 | _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(?<!\\)#(end).*\n?)|' |
---|
1209 | r'(?:^[ \t]*(?<!\\)#((?:\w+|#).*)\n?)', |
---|
1210 | re.MULTILINE) |
---|
1211 | |
---|
1212 | def _parse(self, encoding): |
---|
1213 | """Parse the template from text input.""" |
---|
1214 | stream = [] # list of events of the "compiled" template |
---|
1215 | dirmap = {} # temporary mapping of directives to elements |
---|
1216 | depth = 0 |
---|
1217 | if not encoding: |
---|
1218 | encoding = 'utf-8' |
---|
1219 | |
---|
1220 | source = self.source.read().decode(encoding, 'replace') |
---|
1221 | offset = 0 |
---|
1222 | lineno = 1 |
---|
1223 | |
---|
1224 | for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)): |
---|
1225 | start, end = mo.span() |
---|
1226 | if start > offset: |
---|
1227 | text = source[offset:start] |
---|
1228 | for kind, data, pos in self._interpolate(text, self.basedir, |
---|
1229 | self.filename, lineno): |
---|
1230 | stream.append((kind, data, pos)) |
---|
1231 | lineno += len(text.splitlines()) |
---|
1232 | |
---|
1233 | text = source[start:end].lstrip()[1:] |
---|
1234 | lineno += len(text.splitlines()) |
---|
1235 | directive = text.split(None, 1) |
---|
1236 | if len(directive) > 1: |
---|
1237 | command, value = directive |
---|
1238 | else: |
---|
1239 | command, value = directive[0], None |
---|
1240 | |
---|
1241 | if command == 'end': |
---|
1242 | depth -= 1 |
---|
1243 | if depth in dirmap: |
---|
1244 | directive, start_offset = dirmap.pop(depth) |
---|
1245 | substream = stream[start_offset:] |
---|
1246 | stream[start_offset:] = [(SUB, ([directive], substream), |
---|
1247 | (self.filepath, lineno, 0))] |
---|
1248 | elif command != '#': |
---|
1249 | cls = self._dir_by_name.get(command) |
---|
1250 | if cls is None: |
---|
1251 | raise BadDirectiveError(command) |
---|
1252 | directive = cls(value, None, self.filepath, lineno, 0) |
---|
1253 | dirmap[depth] = (directive, len(stream)) |
---|
1254 | depth += 1 |
---|
1255 | |
---|
1256 | offset = end |
---|
1257 | |
---|
1258 | if offset < len(source): |
---|
1259 | text = source[offset:].replace('\\#', '#') |
---|
1260 | for kind, data, pos in self._interpolate(text, self.basedir, |
---|
1261 | self.filename, lineno): |
---|
1262 | stream.append((kind, data, pos)) |
---|
1263 | |
---|
1264 | return stream |
---|
1265 | |
---|
1266 | |
---|
1267 | class TemplateLoader(object): |
---|
1268 | """Responsible for loading templates from files on the specified search |
---|
1269 | path. |
---|
1270 | |
---|
1271 | >>> import tempfile |
---|
1272 | >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template') |
---|
1273 | >>> os.write(fd, '<p>$var</p>') |
---|
1274 | 11 |
---|
1275 | >>> os.close(fd) |
---|
1276 | |
---|
1277 | The template loader accepts a list of directory paths that are then used |
---|
1278 | when searching for template files, in the given order: |
---|
1279 | |
---|
1280 | >>> loader = TemplateLoader([os.path.dirname(path)]) |
---|
1281 | |
---|
1282 | The `load()` method first checks the template cache whether the requested |
---|
1283 | template has already been loaded. If not, it attempts to locate the |
---|
1284 | template file, and returns the corresponding `Template` object: |
---|
1285 | |
---|
1286 | >>> template = loader.load(os.path.basename(path)) |
---|
1287 | >>> isinstance(template, MarkupTemplate) |
---|
1288 | True |
---|
1289 | |
---|
1290 | Template instances are cached: requesting a template with the same name |
---|
1291 | results in the same instance being returned: |
---|
1292 | |
---|
1293 | >>> loader.load(os.path.basename(path)) is template |
---|
1294 | True |
---|
1295 | |
---|
1296 | >>> os.remove(path) |
---|
1297 | """ |
---|
1298 | def __init__(self, search_path=None, auto_reload=False, |
---|
1299 | default_encoding=None): |
---|
1300 | """Create the template laoder. |
---|
1301 | |
---|
1302 | @param search_path: a list of absolute path names that should be |
---|
1303 | searched for template files |
---|
1304 | @param auto_reload: whether to check the last modification time of |
---|
1305 | template files, and reload them if they have changed |
---|
1306 | @param default_encoding: the default encoding to assume when loading |
---|
1307 | templates; defaults to UTF-8 |
---|
1308 | """ |
---|
1309 | self.search_path = search_path |
---|
1310 | if self.search_path is None: |
---|
1311 | self.search_path = [] |
---|
1312 | self.auto_reload = auto_reload |
---|
1313 | self.default_encoding = default_encoding |
---|
1314 | self._cache = {} |
---|
1315 | self._mtime = {} |
---|
1316 | |
---|
1317 | def load(self, filename, relative_to=None, cls=MarkupTemplate, |
---|
1318 | encoding=None): |
---|
1319 | """Load the template with the given name. |
---|
1320 | |
---|
1321 | If the `filename` parameter is relative, this method searches the search |
---|
1322 | path trying to locate a template matching the given name. If the file |
---|
1323 | name is an absolute path, the search path is not bypassed. |
---|
1324 | |
---|
1325 | If requested template is not found, a `TemplateNotFound` exception is |
---|
1326 | raised. Otherwise, a `Template` object is returned that represents the |
---|
1327 | parsed template. |
---|
1328 | |
---|
1329 | Template instances are cached to avoid having to parse the same |
---|
1330 | template file more than once. Thus, subsequent calls of this method |
---|
1331 | with the same template file name will return the same `Template` |
---|
1332 | object (unless the `auto_reload` option is enabled and the file was |
---|
1333 | changed since the last parse.) |
---|
1334 | |
---|
1335 | If the `relative_to` parameter is provided, the `filename` is |
---|
1336 | interpreted as being relative to that path. |
---|
1337 | |
---|
1338 | @param filename: the relative path of the template file to load |
---|
1339 | @param relative_to: the filename of the template from which the new |
---|
1340 | template is being loaded, or `None` if the template is being loaded |
---|
1341 | directly |
---|
1342 | @param cls: the class of the template object to instantiate |
---|
1343 | @param encoding: the encoding of the template to load; defaults to the |
---|
1344 | `default_encoding` of the loader instance |
---|
1345 | """ |
---|
1346 | if encoding is None: |
---|
1347 | encoding = self.default_encoding |
---|
1348 | if relative_to and not os.path.isabs(relative_to): |
---|
1349 | filename = os.path.join(os.path.dirname(relative_to), filename) |
---|
1350 | filename = os.path.normpath(filename) |
---|
1351 | |
---|
1352 | # First check the cache to avoid reparsing the same file |
---|
1353 | try: |
---|
1354 | tmpl = self._cache[filename] |
---|
1355 | if not self.auto_reload or \ |
---|
1356 | os.path.getmtime(tmpl.filepath) == self._mtime[filename]: |
---|
1357 | return tmpl |
---|
1358 | except KeyError: |
---|
1359 | pass |
---|
1360 | |
---|
1361 | search_path = self.search_path |
---|
1362 | isabs = False |
---|
1363 | |
---|
1364 | if os.path.isabs(filename): |
---|
1365 | # Bypass the search path if the requested filename is absolute |
---|
1366 | search_path = [os.path.dirname(filename)] |
---|
1367 | isabs = True |
---|
1368 | |
---|
1369 | elif relative_to and os.path.isabs(relative_to): |
---|
1370 | # Make sure that the directory containing the including |
---|
1371 | # template is on the search path |
---|
1372 | dirname = os.path.dirname(relative_to) |
---|
1373 | if dirname not in search_path: |
---|
1374 | search_path = search_path + [dirname] |
---|
1375 | isabs = True |
---|
1376 | |
---|
1377 | elif not search_path: |
---|
1378 | # Uh oh, don't know where to look for the template |
---|
1379 | raise TemplateError('Search path for templates not configured') |
---|
1380 | |
---|
1381 | for dirname in search_path: |
---|
1382 | filepath = os.path.join(dirname, filename) |
---|
1383 | try: |
---|
1384 | fileobj = open(filepath, 'U') |
---|
1385 | try: |
---|
1386 | if isabs: |
---|
1387 | # If the filename of either the included or the |
---|
1388 | # including template is absolute, make sure the |
---|
1389 | # included template gets an absolute path, too, |
---|
1390 | # so that nested include work properly without a |
---|
1391 | # search path |
---|
1392 | filename = os.path.join(dirname, filename) |
---|
1393 | dirname = '' |
---|
1394 | tmpl = cls(fileobj, basedir=dirname, filename=filename, |
---|
1395 | loader=self, encoding=encoding) |
---|
1396 | finally: |
---|
1397 | fileobj.close() |
---|
1398 | self._cache[filename] = tmpl |
---|
1399 | self._mtime[filename] = os.path.getmtime(filepath) |
---|
1400 | return tmpl |
---|
1401 | except IOError: |
---|
1402 | continue |
---|
1403 | |
---|
1404 | raise TemplateNotFound(filename, search_path) |
---|