Edgewall Software

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

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

Ported [704] to 0.4.x branch.

  • Property svn:eol-style set to native
File size: 14.4 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2007 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Markup templating engine."""
15
16from itertools import chain
17import sys
18from textwrap import dedent
19
20from genshi.core import Attrs, Namespace, Stream, StreamEventKind
21from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT
22from genshi.input import XMLParser
23from genshi.template.base import BadDirectiveError, Template, \
24                                 TemplateSyntaxError, _apply_directives, SUB
25from genshi.template.eval import Suite
26from genshi.template.interpolation import interpolate
27from genshi.template.loader import TemplateNotFound
28from genshi.template.directives import *
29
30if sys.version_info < (2, 4):
31    _ctxt2dict = lambda ctxt: ctxt.frames[0]
32else:
33    _ctxt2dict = lambda ctxt: ctxt
34
35__all__ = ['MarkupTemplate']
36__docformat__ = 'restructuredtext en'
37
38
39class MarkupTemplate(Template):
40    """Implementation of the template language for XML-based templates.
41   
42    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
43    ...   <li py:for="item in items">${item}</li>
44    ... </ul>''')
45    >>> print tmpl.generate(items=[1, 2, 3])
46    <ul>
47      <li>1</li><li>2</li><li>3</li>
48    </ul>
49    """
50    EXEC = StreamEventKind('EXEC')
51    """Stream event kind representing a Python code suite to execute."""
52
53    INCLUDE = StreamEventKind('INCLUDE')
54    """Stream event kind representing the inclusion of another template."""
55
56    DIRECTIVE_NAMESPACE = Namespace('http://genshi.edgewall.org/')
57    XINCLUDE_NAMESPACE = Namespace('http://www.w3.org/2001/XInclude')
58
59    directives = [('def', DefDirective),
60                  ('match', MatchDirective),
61                  ('when', WhenDirective),
62                  ('otherwise', OtherwiseDirective),
63                  ('for', ForDirective),
64                  ('if', IfDirective),
65                  ('choose', ChooseDirective),
66                  ('with', WithDirective),
67                  ('replace', ReplaceDirective),
68                  ('content', ContentDirective),
69                  ('attrs', AttrsDirective),
70                  ('strip', StripDirective)]
71
72    def __init__(self, source, basedir=None, filename=None, loader=None,
73                 encoding=None, lookup='lenient'):
74        Template.__init__(self, source, basedir=basedir, filename=filename,
75                          loader=loader, encoding=encoding, lookup=lookup)
76
77        self.filters += [self._exec, self._match]
78        if loader:
79            self.filters.append(self._include)
80
81    def _parse(self, source, encoding):
82        streams = [[]] # stacked lists of events of the "compiled" template
83        dirmap = {} # temporary mapping of directives to elements
84        ns_prefix = {}
85        depth = 0
86        fallbacks = []
87        includes = []
88
89        if not isinstance(source, Stream):
90            source = XMLParser(source, filename=self.filename,
91                               encoding=encoding)
92
93        for kind, data, pos in source:
94            stream = streams[-1]
95
96            if kind is START_NS:
97                # Strip out the namespace declaration for template directives
98                prefix, uri = data
99                ns_prefix[prefix] = uri
100                if uri not in (self.DIRECTIVE_NAMESPACE,
101                               self.XINCLUDE_NAMESPACE):
102                    stream.append((kind, data, pos))
103
104            elif kind is END_NS:
105                uri = ns_prefix.pop(data, None)
106                if uri and uri not in (self.DIRECTIVE_NAMESPACE,
107                                       self.XINCLUDE_NAMESPACE):
108                    stream.append((kind, data, pos))
109
110            elif kind is START:
111                # Record any directive attributes in start tags
112                tag, attrs = data
113                directives = []
114                strip = False
115
116                if tag in self.DIRECTIVE_NAMESPACE:
117                    cls = self._dir_by_name.get(tag.localname)
118                    if cls is None:
119                        raise BadDirectiveError(tag.localname, self.filepath,
120                                                pos[1])
121                    value = attrs.get(getattr(cls, 'ATTRIBUTE', None), '')
122                    directives.append((cls, value, ns_prefix.copy(), pos))
123                    strip = True
124
125                new_attrs = []
126                for name, value in attrs:
127                    if name in self.DIRECTIVE_NAMESPACE:
128                        cls = self._dir_by_name.get(name.localname)
129                        if cls is None:
130                            raise BadDirectiveError(name.localname,
131                                                    self.filepath, pos[1])
132                        directives.append((cls, value, ns_prefix.copy(), pos))
133                    else:
134                        if value:
135                            value = list(interpolate(value, self.basedir,
136                                                     pos[0], pos[1], pos[2],
137                                                     lookup=self.lookup))
138                            if len(value) == 1 and value[0][0] is TEXT:
139                                value = value[0][1]
140                        else:
141                            value = [(TEXT, u'', pos)]
142                        new_attrs.append((name, value))
143                new_attrs = Attrs(new_attrs)
144
145                if directives:
146                    index = self._dir_order.index
147                    directives.sort(lambda a, b: cmp(index(a[0]), index(b[0])))
148                    dirmap[(depth, tag)] = (directives, len(stream), strip)
149
150                if tag in self.XINCLUDE_NAMESPACE:
151                    if tag.localname == 'include':
152                        include_href = new_attrs.get('href')
153                        if not include_href:
154                            raise TemplateSyntaxError('Include misses required '
155                                                      'attribute "href"',
156                                                      self.filepath, *pos[1:])
157                        includes.append(include_href)
158                        streams.append([])
159                    elif tag.localname == 'fallback':
160                        streams.append([])
161                        fallbacks.append(streams[-1])
162
163                else:
164                    stream.append((kind, (tag, new_attrs), pos))
165
166                depth += 1
167
168            elif kind is END:
169                depth -= 1
170
171                if fallbacks and data == self.XINCLUDE_NAMESPACE['fallback']:
172                    assert streams.pop() is fallbacks[-1]
173                elif data == self.XINCLUDE_NAMESPACE['include']:
174                    fallback = None
175                    if len(fallbacks) == len(includes):
176                        fallback = fallbacks.pop()
177                    streams.pop() # discard anything between the include tags
178                                  # and the fallback element
179                    stream = streams[-1]
180                    stream.append((INCLUDE, (includes.pop(), fallback), pos))
181                else:
182                    stream.append((kind, data, pos))
183
184                # If there have have directive attributes with the corresponding
185                # start tag, move the events inbetween into a "subprogram"
186                if (depth, data) in dirmap:
187                    directives, start_offset, strip = dirmap.pop((depth, data))
188                    substream = stream[start_offset:]
189                    if strip:
190                        substream = substream[1:-1]
191                    stream[start_offset:] = [(SUB, (directives, substream),
192                                              pos)]
193
194            elif kind is PI and data[0] == 'python':
195                try:
196                    # As Expat doesn't report whitespace between the PI target
197                    # and the data, we have to jump through some hoops here to
198                    # get correctly indented Python code
199                    # Unfortunately, we'll still probably not get the line
200                    # number quite right
201                    lines = [line.expandtabs() for line in data[1].splitlines()]
202                    first = lines[0]
203                    rest = dedent('\n'.join(lines[1:])).rstrip()
204                    if first.rstrip().endswith(':') and not rest[0].isspace():
205                        rest = '\n'.join(['    ' + line for line
206                                          in rest.splitlines()])
207                    source = '\n'.join([first, rest])
208                    suite = Suite(source, self.filepath, pos[1],
209                                  lookup=self.lookup)
210                except SyntaxError, err:
211                    raise TemplateSyntaxError(err, self.filepath,
212                                              pos[1] + (err.lineno or 1) - 1,
213                                              pos[2] + (err.offset or 0))
214                stream.append((EXEC, suite, pos))
215
216            elif kind is TEXT:
217                for kind, data, pos in interpolate(data, self.basedir, pos[0],
218                                                   pos[1], pos[2],
219                                                   lookup=self.lookup):
220                    stream.append((kind, data, pos))
221
222            elif kind is COMMENT:
223                if not data.lstrip().startswith('!'):
224                    stream.append((kind, data, pos))
225
226            else:
227                stream.append((kind, data, pos))
228
229        assert len(streams) == 1
230        return streams[0]
231
232    def _prepare(self, stream):
233        for kind, data, pos in Template._prepare(self, stream):
234            if kind is INCLUDE and data[1]:
235                data = data[0], list(self._prepare(data[1]))
236            yield kind, data, pos
237
238    def _exec(self, stream, ctxt):
239        """Internal stream filter that executes code in ``<?python ?>``
240        processing instructions.
241        """
242        for event in stream:
243            if event[0] is EXEC:
244                event[1].execute(_ctxt2dict(ctxt))
245            else:
246                yield event
247
248    def _include(self, stream, ctxt):
249        """Internal stream filter that performs inclusion of external
250        template files.
251        """
252        for event in stream:
253            if event[0] is INCLUDE:
254                href, fallback = event[1]
255                if not isinstance(href, basestring):
256                    parts = []
257                    for subkind, subdata, subpos in self._eval(href, ctxt):
258                        if subkind is TEXT:
259                            parts.append(subdata)
260                    href = u''.join([x for x in parts if x is not None])
261                try:
262                    tmpl = self.loader.load(href, relative_to=event[2][0])
263                    for event in tmpl.generate(ctxt):
264                        yield event
265                except TemplateNotFound:
266                    if fallback is None:
267                        raise
268                    for filter_ in self.filters:
269                        fallback = filter_(iter(fallback), ctxt)
270                    for event in fallback:
271                        yield event
272            else:
273                yield event
274
275    def _match(self, stream, ctxt, match_templates=None):
276        """Internal stream filter that applies any defined match templates
277        to the stream.
278        """
279        if match_templates is None:
280            match_templates = ctxt._match_templates
281
282        tail = []
283        def _strip(stream):
284            depth = 1
285            while 1:
286                event = stream.next()
287                if event[0] is START:
288                    depth += 1
289                elif event[0] is END:
290                    depth -= 1
291                if depth > 0:
292                    yield event
293                else:
294                    tail[:] = [event]
295                    break
296
297        for event in stream:
298
299            # We (currently) only care about start and end events for matching
300            # We might care about namespace events in the future, though
301            if not match_templates or (event[0] is not START and
302                                       event[0] is not END):
303                yield event
304                continue
305
306            for idx, (test, path, template, namespaces, directives) in \
307                    enumerate(match_templates):
308
309                if test(event, namespaces, ctxt) is True:
310
311                    # Let the remaining match templates know about the event so
312                    # they get a chance to update their internal state
313                    for test in [mt[0] for mt in match_templates[idx + 1:]]:
314                        test(event, namespaces, ctxt, updateonly=True)
315
316                    # Consume and store all events until an end event
317                    # corresponding to this start event is encountered
318                    content = chain([event],
319                                    self._match(_strip(stream), ctxt,
320                                                [match_templates[idx]]),
321                                    tail)
322                    content = list(self._include(content, ctxt))
323
324                    for test in [mt[0] for mt in match_templates]:
325                        test(tail[0], namespaces, ctxt, updateonly=True)
326
327                    # Make the select() function available in the body of the
328                    # match template
329                    def select(path):
330                        return Stream(content).select(path, namespaces, ctxt)
331                    ctxt.push(dict(select=select))
332
333                    # Recursively process the output
334                    template = _apply_directives(template, ctxt, directives)
335                    for event in self._match(self._eval(self._flatten(template,
336                                                                      ctxt),
337                                                        ctxt), ctxt,
338                                             match_templates[:idx] +
339                                             match_templates[idx + 1:]):
340                        yield event
341
342                    ctxt.pop()
343                    break
344
345            else: # no matches
346                yield event
347
348
349EXEC = MarkupTemplate.EXEC
350INCLUDE = MarkupTemplate.INCLUDE
Note: See TracBrowser for help on using the repository browser.