Edgewall Software

source: tags/0.3.0/genshi/template.py

Last change on this file was 308, checked in by cmlenz, 17 years ago
  • Add MANIFEST.in so that the generated HTML documentation is included in the source distribution.
  • Remove duplicate _match implementation.
  • Allow shorthand expressions to start with an underscore.
  • Property svn:eol-style set to native
File size: 46.2 KB
Line 
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
16try:
17    from collections import deque
18except ImportError:
19    class deque(list):
20        def appendleft(self, x): self.insert(0, x)
21        def popleft(self): return self.pop(0)
22import compiler
23import os
24import re
25from StringIO import StringIO
26
27from genshi.core import Attrs, Namespace, Stream, StreamEventKind, _ensure
28from genshi.core import START, END, START_NS, END_NS, TEXT, COMMENT
29from genshi.eval import Expression
30from genshi.input import XMLParser
31from genshi.path import Path
32
33__all__ = ['BadDirectiveError', 'TemplateError', 'TemplateSyntaxError',
34           'TemplateNotFound', 'MarkupTemplate', 'TextTemplate',
35           'TemplateLoader']
36
37
38class TemplateError(Exception):
39    """Base exception class for errors related to template processing."""
40
41
42class 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
57class 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
70class 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
78class 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
143class 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
189def _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
195def _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
213class 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
267class 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
298class 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
394class 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
439class 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
461class 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
494class 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
526class 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
577class 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
628class 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
660class 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
679class 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
733class 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
744class 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
922EXPR = Template.EXPR
923SUB = Template.SUB
924
925
926class 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
1124class 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
1210class 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)
Note: See TracBrowser for help on using the repository browser.