Edgewall Software

source: branches/stable/0.4.x/genshi/template/directives.py

Last change on this file was 580, checked in by cmlenz, 16 years ago

Ported [579] (for #116) to 0.4.x.

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