| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2006-2008 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 various template directives.""" |
|---|
| 15 | |
|---|
| 16 | import compiler |
|---|
| 17 | try: |
|---|
| 18 | frozenset |
|---|
| 19 | except NameError: |
|---|
| 20 | from sets import ImmutableSet as frozenset |
|---|
| 21 | |
|---|
| 22 | from genshi.core import QName, Stream |
|---|
| 23 | from genshi.path import Path |
|---|
| 24 | from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \ |
|---|
| 25 | EXPR, _apply_directives, _eval_expr, \ |
|---|
| 26 | _exec_suite |
|---|
| 27 | from genshi.template.eval import Expression, ExpressionASTTransformer, _parse |
|---|
| 28 | |
|---|
| 29 | __all__ = ['AttrsDirective', 'ChooseDirective', 'ContentDirective', |
|---|
| 30 | 'DefDirective', 'ForDirective', 'IfDirective', 'MatchDirective', |
|---|
| 31 | 'OtherwiseDirective', 'ReplaceDirective', 'StripDirective', |
|---|
| 32 | 'WhenDirective', 'WithDirective'] |
|---|
| 33 | __docformat__ = 'restructuredtext en' |
|---|
| 34 | |
|---|
| 35 | |
|---|
| 36 | class DirectiveMeta(type): |
|---|
| 37 | """Meta class for template directives.""" |
|---|
| 38 | |
|---|
| 39 | def __new__(cls, name, bases, d): |
|---|
| 40 | d['tagname'] = name.lower().replace('directive', '') |
|---|
| 41 | return type.__new__(cls, name, bases, d) |
|---|
| 42 | |
|---|
| 43 | |
|---|
| 44 | class Directive(object): |
|---|
| 45 | """Abstract base class for template directives. |
|---|
| 46 | |
|---|
| 47 | A directive is basically a callable that takes three positional arguments: |
|---|
| 48 | ``ctxt`` is the template data context, ``stream`` is an iterable over the |
|---|
| 49 | events that the directive applies to, and ``directives`` is is a list of |
|---|
| 50 | other directives on the same stream that need to be applied. |
|---|
| 51 | |
|---|
| 52 | Directives can be "anonymous" or "registered". Registered directives can be |
|---|
| 53 | applied by the template author using an XML attribute with the |
|---|
| 54 | corresponding name in the template. Such directives should be subclasses of |
|---|
| 55 | this base class that can be instantiated with the value of the directive |
|---|
| 56 | attribute as parameter. |
|---|
| 57 | |
|---|
| 58 | Anonymous directives are simply functions conforming to the protocol |
|---|
| 59 | described above, and can only be applied programmatically (for example by |
|---|
| 60 | template filters). |
|---|
| 61 | """ |
|---|
| 62 | __metaclass__ = DirectiveMeta |
|---|
| 63 | __slots__ = ['expr'] |
|---|
| 64 | |
|---|
| 65 | def __init__(self, value, template=None, namespaces=None, lineno=-1, |
|---|
| 66 | offset=-1): |
|---|
| 67 | self.expr = self._parse_expr(value, template, lineno, offset) |
|---|
| 68 | |
|---|
| 69 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 70 | """Called after the template stream has been completely parsed. |
|---|
| 71 | |
|---|
| 72 | :param template: the `Template` object |
|---|
| 73 | :param stream: the event stream associated with the directive |
|---|
| 74 | :param value: the argument value for the directive; if the directive was |
|---|
| 75 | specified as an element, this will be an `Attrs` instance |
|---|
| 76 | with all specified attributes, otherwise it will be a |
|---|
| 77 | `unicode` object with just the attribute value |
|---|
| 78 | :param namespaces: a mapping of namespace URIs to prefixes |
|---|
| 79 | :param pos: a ``(filename, lineno, offset)`` tuple describing the |
|---|
| 80 | location where the directive was found in the source |
|---|
| 81 | |
|---|
| 82 | This class method should return a ``(directive, stream)`` tuple. If |
|---|
| 83 | ``directive`` is not ``None``, it should be an instance of the `Directive` |
|---|
| 84 | class, and gets added to the list of directives applied to the substream |
|---|
| 85 | at runtime. `stream` is an event stream that replaces the original |
|---|
| 86 | stream associated with the directive. |
|---|
| 87 | """ |
|---|
| 88 | return cls(value, template, namespaces, *pos[1:]), stream |
|---|
| 89 | attach = classmethod(attach) |
|---|
| 90 | |
|---|
| 91 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 92 | """Apply the directive to the given stream. |
|---|
| 93 | |
|---|
| 94 | :param stream: the event stream |
|---|
| 95 | :param directives: a list of the remaining directives that should |
|---|
| 96 | process the stream |
|---|
| 97 | :param ctxt: the context data |
|---|
| 98 | :param vars: additional variables that should be made available when |
|---|
| 99 | Python code is executed |
|---|
| 100 | """ |
|---|
| 101 | raise NotImplementedError |
|---|
| 102 | |
|---|
| 103 | def __repr__(self): |
|---|
| 104 | expr = '' |
|---|
| 105 | if getattr(self, 'expr', None) is not None: |
|---|
| 106 | expr = ' "%s"' % self.expr.source |
|---|
| 107 | return '<%s%s>' % (self.__class__.__name__, expr) |
|---|
| 108 | |
|---|
| 109 | def _parse_expr(cls, expr, template, lineno=-1, offset=-1): |
|---|
| 110 | """Parses the given expression, raising a useful error message when a |
|---|
| 111 | syntax error is encountered. |
|---|
| 112 | """ |
|---|
| 113 | try: |
|---|
| 114 | return expr and Expression(expr, template.filepath, lineno, |
|---|
| 115 | lookup=template.lookup) or None |
|---|
| 116 | except SyntaxError, err: |
|---|
| 117 | err.msg += ' in expression "%s" of "%s" directive' % (expr, |
|---|
| 118 | cls.tagname) |
|---|
| 119 | raise TemplateSyntaxError(err, template.filepath, lineno, |
|---|
| 120 | offset + (err.offset or 0)) |
|---|
| 121 | _parse_expr = classmethod(_parse_expr) |
|---|
| 122 | |
|---|
| 123 | |
|---|
| 124 | def _assignment(ast): |
|---|
| 125 | """Takes the AST representation of an assignment, and returns a function |
|---|
| 126 | that applies the assignment of a given value to a dictionary. |
|---|
| 127 | """ |
|---|
| 128 | def _names(node): |
|---|
| 129 | if isinstance(node, (compiler.ast.AssTuple, compiler.ast.Tuple)): |
|---|
| 130 | return tuple([_names(child) for child in node.nodes]) |
|---|
| 131 | elif isinstance(node, (compiler.ast.AssName, compiler.ast.Name)): |
|---|
| 132 | return node.name |
|---|
| 133 | def _assign(data, value, names=_names(ast)): |
|---|
| 134 | if type(names) is tuple: |
|---|
| 135 | for idx in range(len(names)): |
|---|
| 136 | _assign(data, value[idx], names[idx]) |
|---|
| 137 | else: |
|---|
| 138 | data[names] = value |
|---|
| 139 | return _assign |
|---|
| 140 | |
|---|
| 141 | |
|---|
| 142 | class AttrsDirective(Directive): |
|---|
| 143 | """Implementation of the ``py:attrs`` template directive. |
|---|
| 144 | |
|---|
| 145 | The value of the ``py:attrs`` attribute should be a dictionary or a sequence |
|---|
| 146 | of ``(name, value)`` tuples. The items in that dictionary or sequence are |
|---|
| 147 | added as attributes to the element: |
|---|
| 148 | |
|---|
| 149 | >>> from genshi.template import MarkupTemplate |
|---|
| 150 | >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 151 | ... <li py:attrs="foo">Bar</li> |
|---|
| 152 | ... </ul>''') |
|---|
| 153 | >>> print tmpl.generate(foo={'class': 'collapse'}) |
|---|
| 154 | <ul> |
|---|
| 155 | <li class="collapse">Bar</li> |
|---|
| 156 | </ul> |
|---|
| 157 | >>> print tmpl.generate(foo=[('class', 'collapse')]) |
|---|
| 158 | <ul> |
|---|
| 159 | <li class="collapse">Bar</li> |
|---|
| 160 | </ul> |
|---|
| 161 | |
|---|
| 162 | If the value evaluates to ``None`` (or any other non-truth value), no |
|---|
| 163 | attributes are added: |
|---|
| 164 | |
|---|
| 165 | >>> print tmpl.generate(foo=None) |
|---|
| 166 | <ul> |
|---|
| 167 | <li>Bar</li> |
|---|
| 168 | </ul> |
|---|
| 169 | """ |
|---|
| 170 | __slots__ = [] |
|---|
| 171 | |
|---|
| 172 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 173 | def _generate(): |
|---|
| 174 | kind, (tag, attrib), pos = stream.next() |
|---|
| 175 | attrs = _eval_expr(self.expr, ctxt, **vars) |
|---|
| 176 | if attrs: |
|---|
| 177 | if isinstance(attrs, Stream): |
|---|
| 178 | try: |
|---|
| 179 | attrs = iter(attrs).next() |
|---|
| 180 | except StopIteration: |
|---|
| 181 | attrs = [] |
|---|
| 182 | elif not isinstance(attrs, list): # assume it's a dict |
|---|
| 183 | attrs = attrs.items() |
|---|
| 184 | attrib -= [name for name, val in attrs if val is None] |
|---|
| 185 | attrib |= [(QName(name), unicode(val).strip()) for name, val |
|---|
| 186 | in attrs if val is not None] |
|---|
| 187 | yield kind, (tag, attrib), pos |
|---|
| 188 | for event in stream: |
|---|
| 189 | yield event |
|---|
| 190 | |
|---|
| 191 | return _apply_directives(_generate(), directives, ctxt, **vars) |
|---|
| 192 | |
|---|
| 193 | |
|---|
| 194 | class ContentDirective(Directive): |
|---|
| 195 | """Implementation of the ``py:content`` template directive. |
|---|
| 196 | |
|---|
| 197 | This directive replaces the content of the element with the result of |
|---|
| 198 | evaluating the value of the ``py:content`` attribute: |
|---|
| 199 | |
|---|
| 200 | >>> from genshi.template import MarkupTemplate |
|---|
| 201 | >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 202 | ... <li py:content="bar">Hello</li> |
|---|
| 203 | ... </ul>''') |
|---|
| 204 | >>> print tmpl.generate(bar='Bye') |
|---|
| 205 | <ul> |
|---|
| 206 | <li>Bye</li> |
|---|
| 207 | </ul> |
|---|
| 208 | """ |
|---|
| 209 | __slots__ = [] |
|---|
| 210 | |
|---|
| 211 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 212 | if type(value) is dict: |
|---|
| 213 | raise TemplateSyntaxError('The content directive can not be used ' |
|---|
| 214 | 'as an element', template.filepath, |
|---|
| 215 | *pos[1:]) |
|---|
| 216 | expr = cls._parse_expr(value, template, *pos[1:]) |
|---|
| 217 | return None, [stream[0], (EXPR, expr, pos), stream[-1]] |
|---|
| 218 | attach = classmethod(attach) |
|---|
| 219 | |
|---|
| 220 | |
|---|
| 221 | class DefDirective(Directive): |
|---|
| 222 | """Implementation of the ``py:def`` template directive. |
|---|
| 223 | |
|---|
| 224 | This directive can be used to create "Named Template Functions", which |
|---|
| 225 | are template snippets that are not actually output during normal |
|---|
| 226 | processing, but rather can be expanded from expressions in other places |
|---|
| 227 | in the template. |
|---|
| 228 | |
|---|
| 229 | A named template function can be used just like a normal Python function |
|---|
| 230 | from template expressions: |
|---|
| 231 | |
|---|
| 232 | >>> from genshi.template import MarkupTemplate |
|---|
| 233 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 234 | ... <p py:def="echo(greeting, name='world')" class="message"> |
|---|
| 235 | ... ${greeting}, ${name}! |
|---|
| 236 | ... </p> |
|---|
| 237 | ... ${echo('Hi', name='you')} |
|---|
| 238 | ... </div>''') |
|---|
| 239 | >>> print tmpl.generate(bar='Bye') |
|---|
| 240 | <div> |
|---|
| 241 | <p class="message"> |
|---|
| 242 | Hi, you! |
|---|
| 243 | </p> |
|---|
| 244 | </div> |
|---|
| 245 | |
|---|
| 246 | If a function does not require parameters, the parenthesis can be omitted |
|---|
| 247 | in the definition: |
|---|
| 248 | |
|---|
| 249 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 250 | ... <p py:def="helloworld" class="message"> |
|---|
| 251 | ... Hello, world! |
|---|
| 252 | ... </p> |
|---|
| 253 | ... ${helloworld()} |
|---|
| 254 | ... </div>''') |
|---|
| 255 | >>> print tmpl.generate(bar='Bye') |
|---|
| 256 | <div> |
|---|
| 257 | <p class="message"> |
|---|
| 258 | Hello, world! |
|---|
| 259 | </p> |
|---|
| 260 | </div> |
|---|
| 261 | """ |
|---|
| 262 | __slots__ = ['name', 'args', 'star_args', 'dstar_args', 'defaults'] |
|---|
| 263 | |
|---|
| 264 | def __init__(self, args, template, namespaces=None, lineno=-1, offset=-1): |
|---|
| 265 | Directive.__init__(self, None, template, namespaces, lineno, offset) |
|---|
| 266 | ast = _parse(args).node |
|---|
| 267 | self.args = [] |
|---|
| 268 | self.star_args = None |
|---|
| 269 | self.dstar_args = None |
|---|
| 270 | self.defaults = {} |
|---|
| 271 | if isinstance(ast, compiler.ast.CallFunc): |
|---|
| 272 | self.name = ast.node.name |
|---|
| 273 | for arg in ast.args: |
|---|
| 274 | if isinstance(arg, compiler.ast.Keyword): |
|---|
| 275 | self.args.append(arg.name) |
|---|
| 276 | self.defaults[arg.name] = Expression(arg.expr, |
|---|
| 277 | template.filepath, |
|---|
| 278 | lineno, |
|---|
| 279 | lookup=template.lookup) |
|---|
| 280 | else: |
|---|
| 281 | self.args.append(arg.name) |
|---|
| 282 | if ast.star_args: |
|---|
| 283 | self.star_args = ast.star_args.name |
|---|
| 284 | if ast.dstar_args: |
|---|
| 285 | self.dstar_args = ast.dstar_args.name |
|---|
| 286 | else: |
|---|
| 287 | self.name = ast.name |
|---|
| 288 | |
|---|
| 289 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 290 | if type(value) is dict: |
|---|
| 291 | value = value.get('function') |
|---|
| 292 | return super(DefDirective, cls).attach(template, stream, value, |
|---|
| 293 | namespaces, pos) |
|---|
| 294 | attach = classmethod(attach) |
|---|
| 295 | |
|---|
| 296 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 297 | stream = list(stream) |
|---|
| 298 | |
|---|
| 299 | def function(*args, **kwargs): |
|---|
| 300 | scope = {} |
|---|
| 301 | args = list(args) # make mutable |
|---|
| 302 | for name in self.args: |
|---|
| 303 | if args: |
|---|
| 304 | scope[name] = args.pop(0) |
|---|
| 305 | else: |
|---|
| 306 | if name in kwargs: |
|---|
| 307 | val = kwargs.pop(name) |
|---|
| 308 | else: |
|---|
| 309 | val = _eval_expr(self.defaults.get(name), ctxt, **vars) |
|---|
| 310 | scope[name] = val |
|---|
| 311 | if not self.star_args is None: |
|---|
| 312 | scope[self.star_args] = args |
|---|
| 313 | if not self.dstar_args is None: |
|---|
| 314 | scope[self.dstar_args] = kwargs |
|---|
| 315 | ctxt.push(scope) |
|---|
| 316 | for event in _apply_directives(stream, directives, ctxt, **vars): |
|---|
| 317 | yield event |
|---|
| 318 | ctxt.pop() |
|---|
| 319 | try: |
|---|
| 320 | function.__name__ = self.name |
|---|
| 321 | except TypeError: |
|---|
| 322 | # Function name can't be set in Python 2.3 |
|---|
| 323 | pass |
|---|
| 324 | |
|---|
| 325 | # Store the function reference in the bottom context frame so that it |
|---|
| 326 | # doesn't get popped off before processing the template has finished |
|---|
| 327 | # FIXME: this makes context data mutable as a side-effect |
|---|
| 328 | ctxt.frames[-1][self.name] = function |
|---|
| 329 | |
|---|
| 330 | return [] |
|---|
| 331 | |
|---|
| 332 | def __repr__(self): |
|---|
| 333 | return '<%s "%s">' % (self.__class__.__name__, self.name) |
|---|
| 334 | |
|---|
| 335 | |
|---|
| 336 | class ForDirective(Directive): |
|---|
| 337 | """Implementation of the ``py:for`` template directive for repeating an |
|---|
| 338 | element based on an iterable in the context data. |
|---|
| 339 | |
|---|
| 340 | >>> from genshi.template import MarkupTemplate |
|---|
| 341 | >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 342 | ... <li py:for="item in items">${item}</li> |
|---|
| 343 | ... </ul>''') |
|---|
| 344 | >>> print tmpl.generate(items=[1, 2, 3]) |
|---|
| 345 | <ul> |
|---|
| 346 | <li>1</li><li>2</li><li>3</li> |
|---|
| 347 | </ul> |
|---|
| 348 | """ |
|---|
| 349 | __slots__ = ['assign', 'filename'] |
|---|
| 350 | |
|---|
| 351 | def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): |
|---|
| 352 | if ' in ' not in value: |
|---|
| 353 | raise TemplateSyntaxError('"in" keyword missing in "for" directive', |
|---|
| 354 | template.filepath, lineno, offset) |
|---|
| 355 | assign, value = value.split(' in ', 1) |
|---|
| 356 | ast = _parse(assign, 'exec') |
|---|
| 357 | value = 'iter(%s)' % value.strip() |
|---|
| 358 | self.assign = _assignment(ast.node.nodes[0].expr) |
|---|
| 359 | self.filename = template.filepath |
|---|
| 360 | Directive.__init__(self, value, template, namespaces, lineno, offset) |
|---|
| 361 | |
|---|
| 362 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 363 | if type(value) is dict: |
|---|
| 364 | value = value.get('each') |
|---|
| 365 | return super(ForDirective, cls).attach(template, stream, value, |
|---|
| 366 | namespaces, pos) |
|---|
| 367 | attach = classmethod(attach) |
|---|
| 368 | |
|---|
| 369 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 370 | iterable = _eval_expr(self.expr, ctxt, **vars) |
|---|
| 371 | if iterable is None: |
|---|
| 372 | return |
|---|
| 373 | |
|---|
| 374 | assign = self.assign |
|---|
| 375 | scope = {} |
|---|
| 376 | stream = list(stream) |
|---|
| 377 | for item in iterable: |
|---|
| 378 | assign(scope, item) |
|---|
| 379 | ctxt.push(scope) |
|---|
| 380 | for event in _apply_directives(stream, directives, ctxt, **vars): |
|---|
| 381 | yield event |
|---|
| 382 | ctxt.pop() |
|---|
| 383 | |
|---|
| 384 | def __repr__(self): |
|---|
| 385 | return '<%s>' % self.__class__.__name__ |
|---|
| 386 | |
|---|
| 387 | |
|---|
| 388 | class IfDirective(Directive): |
|---|
| 389 | """Implementation of the ``py:if`` template directive for conditionally |
|---|
| 390 | excluding elements from being output. |
|---|
| 391 | |
|---|
| 392 | >>> from genshi.template import MarkupTemplate |
|---|
| 393 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 394 | ... <b py:if="foo">${bar}</b> |
|---|
| 395 | ... </div>''') |
|---|
| 396 | >>> print tmpl.generate(foo=True, bar='Hello') |
|---|
| 397 | <div> |
|---|
| 398 | <b>Hello</b> |
|---|
| 399 | </div> |
|---|
| 400 | """ |
|---|
| 401 | __slots__ = [] |
|---|
| 402 | |
|---|
| 403 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 404 | if type(value) is dict: |
|---|
| 405 | value = value.get('test') |
|---|
| 406 | return super(IfDirective, cls).attach(template, stream, value, |
|---|
| 407 | namespaces, pos) |
|---|
| 408 | attach = classmethod(attach) |
|---|
| 409 | |
|---|
| 410 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 411 | value = _eval_expr(self.expr, ctxt, **vars) |
|---|
| 412 | if value: |
|---|
| 413 | return _apply_directives(stream, directives, ctxt, **vars) |
|---|
| 414 | return [] |
|---|
| 415 | |
|---|
| 416 | |
|---|
| 417 | class MatchDirective(Directive): |
|---|
| 418 | """Implementation of the ``py:match`` template directive. |
|---|
| 419 | |
|---|
| 420 | >>> from genshi.template import MarkupTemplate |
|---|
| 421 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 422 | ... <span py:match="greeting"> |
|---|
| 423 | ... Hello ${select('@name')} |
|---|
| 424 | ... </span> |
|---|
| 425 | ... <greeting name="Dude" /> |
|---|
| 426 | ... </div>''') |
|---|
| 427 | >>> print tmpl.generate() |
|---|
| 428 | <div> |
|---|
| 429 | <span> |
|---|
| 430 | Hello Dude |
|---|
| 431 | </span> |
|---|
| 432 | </div> |
|---|
| 433 | """ |
|---|
| 434 | __slots__ = ['path', 'namespaces', 'hints'] |
|---|
| 435 | |
|---|
| 436 | def __init__(self, value, template, hints=None, namespaces=None, |
|---|
| 437 | lineno=-1, offset=-1): |
|---|
| 438 | Directive.__init__(self, None, template, namespaces, lineno, offset) |
|---|
| 439 | self.path = Path(value, template.filepath, lineno) |
|---|
| 440 | self.namespaces = namespaces or {} |
|---|
| 441 | self.hints = hints or () |
|---|
| 442 | |
|---|
| 443 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 444 | hints = [] |
|---|
| 445 | if type(value) is dict: |
|---|
| 446 | if value.get('buffer', '').lower() == 'false': |
|---|
| 447 | hints.append('not_buffered') |
|---|
| 448 | if value.get('once', '').lower() == 'true': |
|---|
| 449 | hints.append('match_once') |
|---|
| 450 | if value.get('recursive', '').lower() == 'false': |
|---|
| 451 | hints.append('not_recursive') |
|---|
| 452 | value = value.get('path') |
|---|
| 453 | return cls(value, template, frozenset(hints), namespaces, *pos[1:]), \ |
|---|
| 454 | stream |
|---|
| 455 | attach = classmethod(attach) |
|---|
| 456 | |
|---|
| 457 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 458 | ctxt._match_templates.append((self.path.test(ignore_context=True), |
|---|
| 459 | self.path, list(stream), self.hints, |
|---|
| 460 | self.namespaces, directives)) |
|---|
| 461 | return [] |
|---|
| 462 | |
|---|
| 463 | def __repr__(self): |
|---|
| 464 | return '<%s "%s">' % (self.__class__.__name__, self.path.source) |
|---|
| 465 | |
|---|
| 466 | |
|---|
| 467 | class ReplaceDirective(Directive): |
|---|
| 468 | """Implementation of the ``py:replace`` template directive. |
|---|
| 469 | |
|---|
| 470 | This directive replaces the element with the result of evaluating the |
|---|
| 471 | value of the ``py:replace`` attribute: |
|---|
| 472 | |
|---|
| 473 | >>> from genshi.template import MarkupTemplate |
|---|
| 474 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 475 | ... <span py:replace="bar">Hello</span> |
|---|
| 476 | ... </div>''') |
|---|
| 477 | >>> print tmpl.generate(bar='Bye') |
|---|
| 478 | <div> |
|---|
| 479 | Bye |
|---|
| 480 | </div> |
|---|
| 481 | |
|---|
| 482 | This directive is equivalent to ``py:content`` combined with ``py:strip``, |
|---|
| 483 | providing a less verbose way to achieve the same effect: |
|---|
| 484 | |
|---|
| 485 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 486 | ... <span py:content="bar" py:strip="">Hello</span> |
|---|
| 487 | ... </div>''') |
|---|
| 488 | >>> print tmpl.generate(bar='Bye') |
|---|
| 489 | <div> |
|---|
| 490 | Bye |
|---|
| 491 | </div> |
|---|
| 492 | """ |
|---|
| 493 | __slots__ = [] |
|---|
| 494 | |
|---|
| 495 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 496 | if type(value) is dict: |
|---|
| 497 | value = value.get('value') |
|---|
| 498 | if not value: |
|---|
| 499 | raise TemplateSyntaxError('missing value for "replace" directive', |
|---|
| 500 | template.filepath, *pos[1:]) |
|---|
| 501 | expr = cls._parse_expr(value, template, *pos[1:]) |
|---|
| 502 | return None, [(EXPR, expr, pos)] |
|---|
| 503 | attach = classmethod(attach) |
|---|
| 504 | |
|---|
| 505 | |
|---|
| 506 | class StripDirective(Directive): |
|---|
| 507 | """Implementation of the ``py:strip`` template directive. |
|---|
| 508 | |
|---|
| 509 | When the value of the ``py:strip`` attribute evaluates to ``True``, the |
|---|
| 510 | element is stripped from the output |
|---|
| 511 | |
|---|
| 512 | >>> from genshi.template import MarkupTemplate |
|---|
| 513 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 514 | ... <div py:strip="True"><b>foo</b></div> |
|---|
| 515 | ... </div>''') |
|---|
| 516 | >>> print tmpl.generate() |
|---|
| 517 | <div> |
|---|
| 518 | <b>foo</b> |
|---|
| 519 | </div> |
|---|
| 520 | |
|---|
| 521 | Leaving the attribute value empty is equivalent to a truth value. |
|---|
| 522 | |
|---|
| 523 | This directive is particulary interesting for named template functions or |
|---|
| 524 | match templates that do not generate a top-level element: |
|---|
| 525 | |
|---|
| 526 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 527 | ... <div py:def="echo(what)" py:strip=""> |
|---|
| 528 | ... <b>${what}</b> |
|---|
| 529 | ... </div> |
|---|
| 530 | ... ${echo('foo')} |
|---|
| 531 | ... </div>''') |
|---|
| 532 | >>> print tmpl.generate() |
|---|
| 533 | <div> |
|---|
| 534 | <b>foo</b> |
|---|
| 535 | </div> |
|---|
| 536 | """ |
|---|
| 537 | __slots__ = [] |
|---|
| 538 | |
|---|
| 539 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 540 | def _generate(): |
|---|
| 541 | if _eval_expr(self.expr, ctxt, **vars): |
|---|
| 542 | stream.next() # skip start tag |
|---|
| 543 | previous = stream.next() |
|---|
| 544 | for event in stream: |
|---|
| 545 | yield previous |
|---|
| 546 | previous = event |
|---|
| 547 | else: |
|---|
| 548 | for event in stream: |
|---|
| 549 | yield event |
|---|
| 550 | return _apply_directives(_generate(), directives, ctxt, **vars) |
|---|
| 551 | |
|---|
| 552 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 553 | if not value: |
|---|
| 554 | return None, stream[1:-1] |
|---|
| 555 | return super(StripDirective, cls).attach(template, stream, value, |
|---|
| 556 | namespaces, pos) |
|---|
| 557 | attach = classmethod(attach) |
|---|
| 558 | |
|---|
| 559 | |
|---|
| 560 | class ChooseDirective(Directive): |
|---|
| 561 | """Implementation of the ``py:choose`` directive for conditionally selecting |
|---|
| 562 | one of several body elements to display. |
|---|
| 563 | |
|---|
| 564 | If the ``py:choose`` expression is empty the expressions of nested |
|---|
| 565 | ``py:when`` directives are tested for truth. The first true ``py:when`` |
|---|
| 566 | body is output. If no ``py:when`` directive is matched then the fallback |
|---|
| 567 | directive ``py:otherwise`` will be used. |
|---|
| 568 | |
|---|
| 569 | >>> from genshi.template import MarkupTemplate |
|---|
| 570 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" |
|---|
| 571 | ... py:choose=""> |
|---|
| 572 | ... <span py:when="0 == 1">0</span> |
|---|
| 573 | ... <span py:when="1 == 1">1</span> |
|---|
| 574 | ... <span py:otherwise="">2</span> |
|---|
| 575 | ... </div>''') |
|---|
| 576 | >>> print tmpl.generate() |
|---|
| 577 | <div> |
|---|
| 578 | <span>1</span> |
|---|
| 579 | </div> |
|---|
| 580 | |
|---|
| 581 | If the ``py:choose`` directive contains an expression, the nested |
|---|
| 582 | ``py:when`` directives are tested for equality to the ``py:choose`` |
|---|
| 583 | expression: |
|---|
| 584 | |
|---|
| 585 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" |
|---|
| 586 | ... py:choose="2"> |
|---|
| 587 | ... <span py:when="1">1</span> |
|---|
| 588 | ... <span py:when="2">2</span> |
|---|
| 589 | ... </div>''') |
|---|
| 590 | >>> print tmpl.generate() |
|---|
| 591 | <div> |
|---|
| 592 | <span>2</span> |
|---|
| 593 | </div> |
|---|
| 594 | |
|---|
| 595 | Behavior is undefined if a ``py:choose`` block contains content outside a |
|---|
| 596 | ``py:when`` or ``py:otherwise`` block. Behavior is also undefined if a |
|---|
| 597 | ``py:otherwise`` occurs before ``py:when`` blocks. |
|---|
| 598 | """ |
|---|
| 599 | __slots__ = ['matched', 'value'] |
|---|
| 600 | |
|---|
| 601 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 602 | if type(value) is dict: |
|---|
| 603 | value = value.get('test') |
|---|
| 604 | return super(ChooseDirective, cls).attach(template, stream, value, |
|---|
| 605 | namespaces, pos) |
|---|
| 606 | attach = classmethod(attach) |
|---|
| 607 | |
|---|
| 608 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 609 | info = [False, bool(self.expr), None] |
|---|
| 610 | if self.expr: |
|---|
| 611 | info[2] = _eval_expr(self.expr, ctxt, **vars) |
|---|
| 612 | ctxt._choice_stack.append(info) |
|---|
| 613 | for event in _apply_directives(stream, directives, ctxt, **vars): |
|---|
| 614 | yield event |
|---|
| 615 | ctxt._choice_stack.pop() |
|---|
| 616 | |
|---|
| 617 | |
|---|
| 618 | class WhenDirective(Directive): |
|---|
| 619 | """Implementation of the ``py:when`` directive for nesting in a parent with |
|---|
| 620 | the ``py:choose`` directive. |
|---|
| 621 | |
|---|
| 622 | See the documentation of the `ChooseDirective` for usage. |
|---|
| 623 | """ |
|---|
| 624 | __slots__ = ['filename'] |
|---|
| 625 | |
|---|
| 626 | def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): |
|---|
| 627 | Directive.__init__(self, value, template, namespaces, lineno, offset) |
|---|
| 628 | self.filename = template.filepath |
|---|
| 629 | |
|---|
| 630 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 631 | if type(value) is dict: |
|---|
| 632 | value = value.get('test') |
|---|
| 633 | return super(WhenDirective, cls).attach(template, stream, value, |
|---|
| 634 | namespaces, pos) |
|---|
| 635 | attach = classmethod(attach) |
|---|
| 636 | |
|---|
| 637 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 638 | info = ctxt._choice_stack and ctxt._choice_stack[-1] |
|---|
| 639 | if not info: |
|---|
| 640 | raise TemplateRuntimeError('"when" directives can only be used ' |
|---|
| 641 | 'inside a "choose" directive', |
|---|
| 642 | self.filename, *stream.next()[2][1:]) |
|---|
| 643 | if info[0]: |
|---|
| 644 | return [] |
|---|
| 645 | if not self.expr and not info[1]: |
|---|
| 646 | raise TemplateRuntimeError('either "choose" or "when" directive ' |
|---|
| 647 | 'must have a test expression', |
|---|
| 648 | self.filename, *stream.next()[2][1:]) |
|---|
| 649 | if info[1]: |
|---|
| 650 | value = info[2] |
|---|
| 651 | if self.expr: |
|---|
| 652 | matched = value == _eval_expr(self.expr, ctxt, **vars) |
|---|
| 653 | else: |
|---|
| 654 | matched = bool(value) |
|---|
| 655 | else: |
|---|
| 656 | matched = bool(_eval_expr(self.expr, ctxt, **vars)) |
|---|
| 657 | info[0] = matched |
|---|
| 658 | if not matched: |
|---|
| 659 | return [] |
|---|
| 660 | |
|---|
| 661 | return _apply_directives(stream, directives, ctxt, **vars) |
|---|
| 662 | |
|---|
| 663 | |
|---|
| 664 | class OtherwiseDirective(Directive): |
|---|
| 665 | """Implementation of the ``py:otherwise`` directive for nesting in a parent |
|---|
| 666 | with the ``py:choose`` directive. |
|---|
| 667 | |
|---|
| 668 | See the documentation of `ChooseDirective` for usage. |
|---|
| 669 | """ |
|---|
| 670 | __slots__ = ['filename'] |
|---|
| 671 | |
|---|
| 672 | def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): |
|---|
| 673 | Directive.__init__(self, None, template, namespaces, lineno, offset) |
|---|
| 674 | self.filename = template.filepath |
|---|
| 675 | |
|---|
| 676 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 677 | info = ctxt._choice_stack and ctxt._choice_stack[-1] |
|---|
| 678 | if not info: |
|---|
| 679 | raise TemplateRuntimeError('an "otherwise" directive can only be ' |
|---|
| 680 | 'used inside a "choose" directive', |
|---|
| 681 | self.filename, *stream.next()[2][1:]) |
|---|
| 682 | if info[0]: |
|---|
| 683 | return [] |
|---|
| 684 | info[0] = True |
|---|
| 685 | |
|---|
| 686 | return _apply_directives(stream, directives, ctxt, **vars) |
|---|
| 687 | |
|---|
| 688 | |
|---|
| 689 | class WithDirective(Directive): |
|---|
| 690 | """Implementation of the ``py:with`` template directive, which allows |
|---|
| 691 | shorthand access to variables and expressions. |
|---|
| 692 | |
|---|
| 693 | >>> from genshi.template import MarkupTemplate |
|---|
| 694 | >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> |
|---|
| 695 | ... <span py:with="y=7; z=x+10">$x $y $z</span> |
|---|
| 696 | ... </div>''') |
|---|
| 697 | >>> print tmpl.generate(x=42) |
|---|
| 698 | <div> |
|---|
| 699 | <span>42 7 52</span> |
|---|
| 700 | </div> |
|---|
| 701 | """ |
|---|
| 702 | __slots__ = ['vars'] |
|---|
| 703 | |
|---|
| 704 | def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): |
|---|
| 705 | Directive.__init__(self, None, template, namespaces, lineno, offset) |
|---|
| 706 | self.vars = [] |
|---|
| 707 | value = value.strip() |
|---|
| 708 | try: |
|---|
| 709 | ast = _parse(value, 'exec').node |
|---|
| 710 | for node in ast.nodes: |
|---|
| 711 | if isinstance(node, compiler.ast.Discard): |
|---|
| 712 | continue |
|---|
| 713 | elif not isinstance(node, compiler.ast.Assign): |
|---|
| 714 | raise TemplateSyntaxError('only assignment allowed in ' |
|---|
| 715 | 'value of the "with" directive', |
|---|
| 716 | template.filepath, lineno, offset) |
|---|
| 717 | self.vars.append(([_assignment(n) for n in node.nodes], |
|---|
| 718 | Expression(node.expr, template.filepath, |
|---|
| 719 | lineno, lookup=template.lookup))) |
|---|
| 720 | except SyntaxError, err: |
|---|
| 721 | err.msg += ' in expression "%s" of "%s" directive' % (value, |
|---|
| 722 | self.tagname) |
|---|
| 723 | raise TemplateSyntaxError(err, template.filepath, lineno, |
|---|
| 724 | offset + (err.offset or 0)) |
|---|
| 725 | |
|---|
| 726 | def attach(cls, template, stream, value, namespaces, pos): |
|---|
| 727 | if type(value) is dict: |
|---|
| 728 | value = value.get('vars') |
|---|
| 729 | return super(WithDirective, cls).attach(template, stream, value, |
|---|
| 730 | namespaces, pos) |
|---|
| 731 | attach = classmethod(attach) |
|---|
| 732 | |
|---|
| 733 | def __call__(self, stream, directives, ctxt, **vars): |
|---|
| 734 | frame = {} |
|---|
| 735 | ctxt.push(frame) |
|---|
| 736 | for targets, expr in self.vars: |
|---|
| 737 | value = _eval_expr(expr, ctxt, **vars) |
|---|
| 738 | for assign in targets: |
|---|
| 739 | assign(frame, value) |
|---|
| 740 | for event in _apply_directives(stream, directives, ctxt, **vars): |
|---|
| 741 | yield event |
|---|
| 742 | ctxt.pop() |
|---|
| 743 | |
|---|
| 744 | def __repr__(self): |
|---|
| 745 | return '<%s>' % (self.__class__.__name__) |
|---|