| 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) |
|---|