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