Edgewall Software

source: tags/0.3.4/genshi/template.py

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

Create tag for 0.3.4 release.

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