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