Edgewall Software

source: tags/0.3.3/genshi/template.py

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

Ported [370] to 0.3.x branch.

  • Property svn:eol-style set to native
File size: 49.2 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Implementation of the template engine."""
15
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', '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 = compiler.parse(args, 'eval').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 = compiler.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 = compiler.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        """Initialize a template from either a string or a file-like object."""
797        if isinstance(source, basestring):
798            self.source = StringIO(source)
799        else:
800            self.source = source
801        self.basedir = basedir
802        self.filename = filename
803        if basedir and filename:
804            self.filepath = os.path.join(basedir, filename)
805        else:
806            self.filepath = filename
807
808        self.filters = [self._flatten, self._eval]
809
810        self.stream = self._parse()
811
812    def __repr__(self):
813        return '<%s "%s">' % (self.__class__.__name__, self.filename)
814
815    def _parse(self):
816        """Parse the template.
817       
818        The parsing stage parses the template and constructs a list of
819        directives that will be executed in the render stage. The input is
820        split up into literal output (text that does not depend on the context
821        data) and directives or expressions.
822        """
823        raise NotImplementedError
824
825    _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}', re.DOTALL)
826    _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z_][a-zA-Z0-9_\.]*)')
827
828    def _interpolate(cls, text, basedir=None, filename=None, lineno=-1,
829                     offset=0):
830        """Parse the given string and extract expressions.
831       
832        This method returns a list containing both literal text and `Expression`
833        objects.
834       
835        @param text: the text to parse
836        @param lineno: the line number at which the text was found (optional)
837        @param offset: the column number at which the text starts in the source
838            (optional)
839        """
840        def _interpolate(text, patterns, filename=filename, lineno=lineno,
841                         offset=offset):
842            for idx, grp in enumerate(patterns.pop(0).split(text)):
843                if idx % 2:
844                    try:
845                        yield EXPR, Expression(grp.strip(), filename, lineno), \
846                              (filename, lineno, offset)
847                    except SyntaxError, err:
848                        raise TemplateSyntaxError(err, filename, lineno,
849                                                  offset + (err.offset or 0))
850                elif grp:
851                    if patterns:
852                        for result in _interpolate(grp, patterns[:]):
853                            yield result
854                    else:
855                        yield TEXT, grp.replace('$$', '$'), \
856                              (filename, lineno, offset)
857                if '\n' in grp:
858                    lines = grp.splitlines()
859                    lineno += len(lines) - 1
860                    offset += len(lines[-1])
861                else:
862                    offset += len(grp)
863        return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE])
864    _interpolate = classmethod(_interpolate)
865
866    def generate(self, *args, **kwargs):
867        """Apply the template to the given context data.
868       
869        Any keyword arguments are made available to the template as context
870        data.
871       
872        Only one positional argument is accepted: if it is provided, it must be
873        an instance of the `Context` class, and keyword arguments are ignored.
874        This calling style is used for internal processing.
875       
876        @return: a markup event stream representing the result of applying
877            the template to the context data.
878        """
879        if args:
880            assert len(args) == 1
881            ctxt = args[0]
882            if ctxt is None:
883                ctxt = Context(**kwargs)
884            assert isinstance(ctxt, Context)
885        else:
886            ctxt = Context(**kwargs)
887
888        stream = self.stream
889        for filter_ in self.filters:
890            stream = filter_(iter(stream), ctxt)
891        return Stream(stream)
892
893    def _eval(self, stream, ctxt):
894        """Internal stream filter that evaluates any expressions in `START` and
895        `TEXT` events.
896        """
897        filters = (self._flatten, self._eval)
898
899        for kind, data, pos in stream:
900
901            if kind is START and data[1]:
902                # Attributes may still contain expressions in start tags at
903                # this point, so do some evaluation
904                tag, attrib = data
905                new_attrib = []
906                for name, substream in attrib:
907                    if isinstance(substream, basestring):
908                        value = substream
909                    else:
910                        values = []
911                        for subkind, subdata, subpos in self._eval(substream,
912                                                                   ctxt):
913                            if subkind is TEXT:
914                                values.append(subdata)
915                        value = [x for x in values if x is not None]
916                        if not value:
917                            continue
918                    new_attrib.append((name, u''.join(value)))
919                yield kind, (tag, Attrs(new_attrib)), pos
920
921            elif kind is EXPR:
922                result = data.evaluate(ctxt)
923                if result is None:
924                    continue
925
926                # First check for a string, otherwise the iterable test below
927                # succeeds, and the string will be chopped up into individual
928                # characters
929                if isinstance(result, basestring):
930                    yield TEXT, result, pos
931                else:
932                    # Test if the expression evaluated to an iterable, in which
933                    # case we yield the individual items
934                    try:
935                        substream = _ensure(iter(result))
936                    except TypeError:
937                        # Neither a string nor an iterable, so just pass it
938                        # through
939                        yield TEXT, unicode(result), pos
940                    else:
941                        for filter_ in filters:
942                            substream = filter_(substream, ctxt)
943                        for event in substream:
944                            yield event
945
946            else:
947                yield kind, data, pos
948
949    def _flatten(self, stream, ctxt):
950        """Internal stream filter that expands `SUB` events in the stream."""
951        for kind, data, pos in stream:
952            if kind is SUB:
953                # This event is a list of directives and a list of nested
954                # events to which those directives should be applied
955                directives, substream = data
956                substream = _apply_directives(substream, ctxt, directives)
957                for event in self._flatten(substream, ctxt):
958                    yield event
959            else:
960                yield kind, data, pos
961
962
963EXPR = Template.EXPR
964SUB = Template.SUB
965
966
967class MarkupTemplate(Template):
968    """Implementation of the template language for XML-based templates.
969   
970    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
971    ...   <li py:for="item in items">${item}</li>
972    ... </ul>''')
973    >>> print tmpl.generate(items=[1, 2, 3])
974    <ul>
975      <li>1</li><li>2</li><li>3</li>
976    </ul>
977    """
978    NAMESPACE = Namespace('http://genshi.edgewall.org/')
979
980    directives = [('def', DefDirective),
981                  ('match', MatchDirective),
982                  ('when', WhenDirective),
983                  ('otherwise', OtherwiseDirective),
984                  ('for', ForDirective),
985                  ('if', IfDirective),
986                  ('choose', ChooseDirective),
987                  ('with', WithDirective),
988                  ('replace', ReplaceDirective),
989                  ('content', ContentDirective),
990                  ('attrs', AttrsDirective),
991                  ('strip', StripDirective)]
992
993    def __init__(self, source, basedir=None, filename=None, loader=None):
994        """Initialize a template from either a string or a file-like object."""
995        Template.__init__(self, source, basedir=basedir, filename=filename,
996                          loader=loader)
997
998        self.filters.append(self._match)
999        if loader:
1000            from genshi.filters import IncludeFilter
1001            self.filters.append(IncludeFilter(loader))
1002
1003    def _parse(self):
1004        """Parse the template from an XML document."""
1005        stream = [] # list of events of the "compiled" template
1006        dirmap = {} # temporary mapping of directives to elements
1007        ns_prefix = {}
1008        depth = 0
1009
1010        for kind, data, pos in XMLParser(self.source, filename=self.filename):
1011
1012            if kind is START_NS:
1013                # Strip out the namespace declaration for template directives
1014                prefix, uri = data
1015                ns_prefix[prefix] = uri
1016                if uri != self.NAMESPACE:
1017                    stream.append((kind, data, pos))
1018
1019            elif kind is END_NS:
1020                uri = ns_prefix.pop(data, None)
1021                if uri and uri != self.NAMESPACE:
1022                    stream.append((kind, data, pos))
1023
1024            elif kind is START:
1025                # Record any directive attributes in start tags
1026                tag, attrib = data
1027                directives = []
1028                strip = False
1029
1030                if tag in self.NAMESPACE:
1031                    cls = self._dir_by_name.get(tag.localname)
1032                    if cls is None:
1033                        raise BadDirectiveError(tag.localname, self.filepath,
1034                                                pos[1])
1035                    value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
1036                    directives.append(cls(value, ns_prefix, self.filepath,
1037                                          pos[1], pos[2]))
1038                    strip = True
1039
1040                new_attrib = []
1041                for name, value in attrib:
1042                    if name in self.NAMESPACE:
1043                        cls = self._dir_by_name.get(name.localname)
1044                        if cls is None:
1045                            raise BadDirectiveError(name.localname,
1046                                                    self.filepath, pos[1])
1047                        directives.append(cls(value, ns_prefix, self.filepath,
1048                                              pos[1], pos[2]))
1049                    else:
1050                        if value:
1051                            value = list(self._interpolate(value, self.basedir,
1052                                                           *pos))
1053                            if len(value) == 1 and value[0][0] is TEXT:
1054                                value = value[0][1]
1055                        else:
1056                            value = [(TEXT, u'', pos)]
1057                        new_attrib.append((name, value))
1058
1059                if directives:
1060                    index = self._dir_order.index
1061                    directives.sort(lambda a, b: cmp(index(a.__class__),
1062                                                     index(b.__class__)))
1063                    dirmap[(depth, tag)] = (directives, len(stream), strip)
1064
1065                stream.append((kind, (tag, Attrs(new_attrib)), pos))
1066                depth += 1
1067
1068            elif kind is END:
1069                depth -= 1
1070                stream.append((kind, data, pos))
1071
1072                # If there have have directive attributes with the corresponding
1073                # start tag, move the events inbetween into a "subprogram"
1074                if (depth, data) in dirmap:
1075                    directives, start_offset, strip = dirmap.pop((depth, data))
1076                    substream = stream[start_offset:]
1077                    if strip:
1078                        substream = substream[1:-1]
1079                    stream[start_offset:] = [(SUB, (directives, substream),
1080                                              pos)]
1081
1082            elif kind is TEXT:
1083                for kind, data, pos in self._interpolate(data, self.basedir,
1084                                                         *pos):
1085                    stream.append((kind, data, pos))
1086
1087            elif kind is COMMENT:
1088                if not data.lstrip().startswith('!'):
1089                    stream.append((kind, data, pos))
1090
1091            else:
1092                stream.append((kind, data, pos))
1093
1094        return stream
1095
1096    def _match(self, stream, ctxt, match_templates=None):
1097        """Internal stream filter that applies any defined match templates
1098        to the stream.
1099        """
1100        if match_templates is None:
1101            match_templates = ctxt._match_templates
1102
1103        tail = []
1104        def _strip(stream):
1105            depth = 1
1106            while 1:
1107                kind, data, pos = stream.next()
1108                if kind is START:
1109                    depth += 1
1110                elif kind is END:
1111                    depth -= 1
1112                if depth > 0:
1113                    yield kind, data, pos
1114                else:
1115                    tail[:] = [(kind, data, pos)]
1116                    break
1117
1118        for kind, data, pos in stream:
1119
1120            # We (currently) only care about start and end events for matching
1121            # We might care about namespace events in the future, though
1122            if not match_templates or kind not in (START, END):
1123                yield kind, data, pos
1124                continue
1125
1126            for idx, (test, path, template, namespaces, directives) in \
1127                    enumerate(match_templates):
1128
1129                if test(kind, data, pos, namespaces, ctxt) is True:
1130
1131                    # Let the remaining match templates know about the event so
1132                    # they get a chance to update their internal state
1133                    for test in [mt[0] for mt in match_templates[idx + 1:]]:
1134                        test(kind, data, pos, namespaces, ctxt)
1135
1136                    # Consume and store all events until an end event
1137                    # corresponding to this start event is encountered
1138                    content = chain([(kind, data, pos)],
1139                                    self._match(_strip(stream), ctxt),
1140                                    tail)
1141                    for filter_ in self.filters[3:]:
1142                        content = filter_(content, ctxt)
1143                    content = list(content)
1144
1145                    kind, data, pos = tail[0]
1146                    for test in [mt[0] for mt in match_templates]:
1147                        test(kind, data, pos, namespaces, ctxt)
1148
1149                    # Make the select() function available in the body of the
1150                    # match template
1151                    def select(path):
1152                        return Stream(content).select(path, namespaces, ctxt)
1153                    ctxt.push(dict(select=select))
1154
1155                    # Recursively process the output
1156                    template = _apply_directives(template, ctxt, directives)
1157                    for event in self._match(self._eval(self._flatten(template,
1158                                                                      ctxt),
1159                                                        ctxt), ctxt,
1160                                             match_templates[:idx] +
1161                                             match_templates[idx + 1:]):
1162                        yield event
1163
1164                    ctxt.pop()
1165                    break
1166
1167            else: # no matches
1168                yield kind, data, pos
1169
1170
1171class TextTemplate(Template):
1172    """Implementation of a simple text-based template engine.
1173   
1174    >>> tmpl = TextTemplate('''Dear $name,
1175    ...
1176    ... We have the following items for you:
1177    ... #for item in items
1178    ...  * $item
1179    ... #end
1180    ...
1181    ... All the best,
1182    ... Foobar''')
1183    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
1184    Dear Joe,
1185    <BLANKLINE>
1186    We have the following items for you:
1187     * 1
1188     * 2
1189     * 3
1190    <BLANKLINE>
1191    All the best,
1192    Foobar
1193    """
1194    directives = [('def', DefDirective),
1195                  ('when', WhenDirective),
1196                  ('otherwise', OtherwiseDirective),
1197                  ('for', ForDirective),
1198                  ('if', IfDirective),
1199                  ('choose', ChooseDirective),
1200                  ('with', WithDirective)]
1201
1202    _DIRECTIVE_RE = re.compile(r'^\s*(?<!\\)#((?:\w+|#).*)\n?', re.MULTILINE)
1203
1204    def _parse(self):
1205        """Parse the template from text input."""
1206        stream = [] # list of events of the "compiled" template
1207        dirmap = {} # temporary mapping of directives to elements
1208        depth = 0
1209
1210        source = self.source.read()
1211        offset = 0
1212        lineno = 1
1213
1214        for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)):
1215            start, end = mo.span()
1216            if start > offset:
1217                text = source[offset:start]
1218                for kind, data, pos in self._interpolate(text, self.basedir,
1219                                                         self.filename, lineno):
1220                    stream.append((kind, data, pos))
1221                lineno += len(text.splitlines())
1222
1223            text = source[start:end].lstrip()[1:]
1224            lineno += len(text.splitlines())
1225            directive = text.split(None, 1)
1226            if len(directive) > 1:
1227                command, value = directive
1228            else:
1229                command, value = directive[0], None
1230
1231            if command == 'end':
1232                depth -= 1
1233                if depth in dirmap:
1234                    directive, start_offset = dirmap.pop(depth)
1235                    substream = stream[start_offset:]
1236                    stream[start_offset:] = [(SUB, ([directive], substream),
1237                                              (self.filepath, lineno, 0))]
1238            elif command != '#':
1239                cls = self._dir_by_name.get(command)
1240                if cls is None:
1241                    raise BadDirectiveError(command)
1242                directive = cls(value, None, self.filepath, lineno, 0)
1243                dirmap[depth] = (directive, len(stream))
1244                depth += 1
1245
1246            offset = end
1247
1248        if offset < len(source):
1249            text = source[offset:].replace('\\#', '#')
1250            for kind, data, pos in self._interpolate(text, self.basedir,
1251                                                     self.filename, lineno):
1252                stream.append((kind, data, pos))
1253
1254        return stream
1255
1256
1257class TemplateLoader(object):
1258    """Responsible for loading templates from files on the specified search
1259    path.
1260   
1261    >>> import tempfile
1262    >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
1263    >>> os.write(fd, '<p>$var</p>')
1264    11
1265    >>> os.close(fd)
1266   
1267    The template loader accepts a list of directory paths that are then used
1268    when searching for template files, in the given order:
1269   
1270    >>> loader = TemplateLoader([os.path.dirname(path)])
1271   
1272    The `load()` method first checks the template cache whether the requested
1273    template has already been loaded. If not, it attempts to locate the
1274    template file, and returns the corresponding `Template` object:
1275   
1276    >>> template = loader.load(os.path.basename(path))
1277    >>> isinstance(template, MarkupTemplate)
1278    True
1279   
1280    Template instances are cached: requesting a template with the same name
1281    results in the same instance being returned:
1282   
1283    >>> loader.load(os.path.basename(path)) is template
1284    True
1285   
1286    >>> os.remove(path)
1287    """
1288    def __init__(self, search_path=None, auto_reload=False):
1289        """Create the template laoder.
1290       
1291        @param search_path: a list of absolute path names that should be
1292            searched for template files
1293        @param auto_reload: whether to check the last modification time of
1294            template files, and reload them if they have changed
1295        """
1296        self.search_path = search_path
1297        if self.search_path is None:
1298            self.search_path = []
1299        self.auto_reload = auto_reload
1300        self._cache = {}
1301        self._mtime = {}
1302
1303    def load(self, filename, relative_to=None, cls=MarkupTemplate):
1304        """Load the template with the given name.
1305       
1306        If the `filename` parameter is relative, this method searches the search
1307        path trying to locate a template matching the given name. If the file
1308        name is an absolute path, the search path is not bypassed.
1309       
1310        If requested template is not found, a `TemplateNotFound` exception is
1311        raised. Otherwise, a `Template` object is returned that represents the
1312        parsed template.
1313       
1314        Template instances are cached to avoid having to parse the same
1315        template file more than once. Thus, subsequent calls of this method
1316        with the same template file name will return the same `Template`
1317        object (unless the `auto_reload` option is enabled and the file was
1318        changed since the last parse.)
1319       
1320        If the `relative_to` parameter is provided, the `filename` is
1321        interpreted as being relative to that path.
1322       
1323        @param filename: the relative path of the template file to load
1324        @param relative_to: the filename of the template from which the new
1325            template is being loaded, or `None` if the template is being loaded
1326            directly
1327        @param cls: the class of the template object to instantiate
1328        """
1329        if relative_to and not os.path.isabs(relative_to):
1330            filename = os.path.join(os.path.dirname(relative_to), filename)
1331        filename = os.path.normpath(filename)
1332
1333        # First check the cache to avoid reparsing the same file
1334        try:
1335            tmpl = self._cache[filename]
1336            if not self.auto_reload or \
1337                    os.path.getmtime(tmpl.filepath) == self._mtime[filename]:
1338                return tmpl
1339        except KeyError:
1340            pass
1341
1342        search_path = self.search_path
1343        isabs = False
1344
1345        if os.path.isabs(filename):
1346            # Bypass the search path if the requested filename is absolute
1347            search_path = [os.path.dirname(filename)]
1348            isabs = True
1349
1350        elif relative_to and os.path.isabs(relative_to):
1351            # Make sure that the directory containing the including
1352            # template is on the search path
1353            dirname = os.path.dirname(relative_to)
1354            if dirname not in search_path:
1355                search_path = search_path + [dirname]
1356            isabs = True
1357
1358        elif not search_path:
1359            # Uh oh, don't know where to look for the template
1360            raise TemplateError('Search path for templates not configured')
1361
1362        for dirname in search_path:
1363            filepath = os.path.join(dirname, filename)
1364            try:
1365                fileobj = open(filepath, 'U')
1366                try:
1367                    if isabs:
1368                        # If the filename of either the included or the
1369                        # including template is absolute, make sure the
1370                        # included template gets an absolute path, too,
1371                        # so that nested include work properly without a
1372                        # search path
1373                        filename = os.path.join(dirname, filename)
1374                        dirname = ''
1375                    tmpl = cls(fileobj, basedir=dirname, filename=filename,
1376                               loader=self)
1377                finally:
1378                    fileobj.close()
1379                self._cache[filename] = tmpl
1380                self._mtime[filename] = os.path.getmtime(filepath)
1381                return tmpl
1382            except IOError:
1383                continue
1384
1385        raise TemplateNotFound(filename, search_path)
Note: See TracBrowser for help on using the repository browser.