Edgewall Software

source: tags/0.3.1/genshi/template.py

Last change on this file was 327, checked in by cmlenz, 17 years ago

Prepare 0.3.1 release.

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