Edgewall Software

source: trunk/genshi/template/markup.py

Last change on this file was 1257, checked in by hodgestar, 10 years ago

Fix infinite recursion in template inlining (fixes #584).

  • Property svn:eol-style set to native
File size: 16.2 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2010 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
17
18from genshi.core import Attrs, Markup, Namespace, Stream, StreamEventKind
19from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT
20from genshi.input import XMLParser
21from genshi.template.base import BadDirectiveError, Template, \
22                                 TemplateSyntaxError, _apply_directives, \
23                                 EXEC, INCLUDE, SUB
24from genshi.template.eval import Suite
25from genshi.template.interpolation import interpolate
26from genshi.template.directives import *
27from genshi.template.text import NewTextTemplate
28
29__all__ = ['MarkupTemplate']
30__docformat__ = 'restructuredtext en'
31
32
33class MarkupTemplate(Template):
34    """Implementation of the template language for XML-based templates.
35   
36    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
37    ...   <li py:for="item in items">${item}</li>
38    ... </ul>''')
39    >>> print(tmpl.generate(items=[1, 2, 3]))
40    <ul>
41      <li>1</li><li>2</li><li>3</li>
42    </ul>
43    """
44
45    DIRECTIVE_NAMESPACE = 'http://genshi.edgewall.org/'
46    XINCLUDE_NAMESPACE = 'http://www.w3.org/2001/XInclude'
47
48    directives = [('def', DefDirective),
49                  ('match', MatchDirective),
50                  ('when', WhenDirective),
51                  ('otherwise', OtherwiseDirective),
52                  ('for', ForDirective),
53                  ('if', IfDirective),
54                  ('choose', ChooseDirective),
55                  ('with', WithDirective),
56                  ('replace', ReplaceDirective),
57                  ('content', ContentDirective),
58                  ('attrs', AttrsDirective),
59                  ('strip', StripDirective)]
60    serializer = 'xml'
61    _number_conv = Markup
62
63    def __init__(self, source, filepath=None, filename=None, loader=None,
64                 encoding=None, lookup='strict', allow_exec=True):
65        Template.__init__(self, source, filepath=filepath, filename=filename,
66                          loader=loader, encoding=encoding, lookup=lookup,
67                          allow_exec=allow_exec)
68        self.add_directives(self.DIRECTIVE_NAMESPACE, self)
69
70    def _init_filters(self):
71        Template._init_filters(self)
72        # Make sure the include filter comes after the match filter
73        self.filters.remove(self._include)
74        self.filters += [self._match, self._include]
75
76    def _parse(self, source, encoding):
77        if not isinstance(source, Stream):
78            source = XMLParser(source, filename=self.filename,
79                               encoding=encoding)
80        stream = []
81
82        for kind, data, pos in source:
83
84            if kind is TEXT:
85                for kind, data, pos in interpolate(data, self.filepath, pos[1],
86                                                   pos[2], lookup=self.lookup):
87                    stream.append((kind, data, pos))
88
89            elif kind is PI and data[0] == 'python':
90                if not self.allow_exec:
91                    raise TemplateSyntaxError('Python code blocks not allowed',
92                                              self.filepath, *pos[1:])
93                try:
94                    suite = Suite(data[1], self.filepath, pos[1],
95                                  lookup=self.lookup)
96                except SyntaxError, err:
97                    raise TemplateSyntaxError(err, self.filepath,
98                                              pos[1] + (err.lineno or 1) - 1,
99                                              pos[2] + (err.offset or 0))
100                stream.append((EXEC, suite, pos))
101
102            elif kind is COMMENT:
103                if not data.lstrip().startswith('!'):
104                    stream.append((kind, data, pos))
105
106            else:
107                stream.append((kind, data, pos))
108
109        return stream
110
111    def _extract_directives(self, stream, namespace, factory):
112        depth = 0
113        dirmap = {} # temporary mapping of directives to elements
114        new_stream = []
115        ns_prefix = {} # namespace prefixes in use
116
117        for kind, data, pos in stream:
118
119            if kind is START:
120                tag, attrs = data
121                directives = []
122                strip = False
123
124                if tag.namespace == namespace:
125                    cls = factory.get_directive(tag.localname)
126                    if cls is None:
127                        raise BadDirectiveError(tag.localname,
128                                                self.filepath, pos[1])
129                    args = dict([(name.localname, value) for name, value
130                                 in attrs if not name.namespace])
131                    directives.append((factory.get_directive_index(cls), cls,
132                                       args, ns_prefix.copy(), pos))
133                    strip = True
134
135                new_attrs = []
136                for name, value in attrs:
137                    if name.namespace == namespace:
138                        cls = factory.get_directive(name.localname)
139                        if cls is None:
140                            raise BadDirectiveError(name.localname,
141                                                    self.filepath, pos[1])
142                        if type(value) is list and len(value) == 1:
143                            value = value[0][1]
144                        directives.append((factory.get_directive_index(cls),
145                                           cls, value, ns_prefix.copy(), pos))
146                    else:
147                        new_attrs.append((name, value))
148                new_attrs = Attrs(new_attrs)
149
150                if directives:
151                    directives.sort()
152                    dirmap[(depth, tag)] = (directives, len(new_stream),
153                                            strip)
154
155                new_stream.append((kind, (tag, new_attrs), pos))
156                depth += 1
157
158            elif kind is END:
159                depth -= 1
160                new_stream.append((kind, data, pos))
161
162                # If there have have directive attributes with the
163                # corresponding start tag, move the events inbetween into
164                # a "subprogram"
165                if (depth, data) in dirmap:
166                    directives, offset, strip = dirmap.pop((depth, data))
167                    substream = new_stream[offset:]
168                    if strip:
169                        substream = substream[1:-1]
170                    new_stream[offset:] = [
171                        (SUB, (directives, substream), pos)
172                    ]
173
174            elif kind is SUB:
175                directives, substream = data
176                substream = self._extract_directives(substream, namespace,
177                                                     factory)
178
179                if len(substream) == 1 and substream[0][0] is SUB:
180                    added_directives, substream = substream[0][1]
181                    directives += added_directives
182
183                new_stream.append((kind, (directives, substream), pos))
184
185            elif kind is START_NS:
186                # Strip out the namespace declaration for template
187                # directives
188                prefix, uri = data
189                ns_prefix[prefix] = uri
190                if uri != namespace:
191                    new_stream.append((kind, data, pos))
192
193            elif kind is END_NS:
194                uri = ns_prefix.pop(data, None)
195                if uri and uri != namespace:
196                    new_stream.append((kind, data, pos))
197
198            else:
199                new_stream.append((kind, data, pos))
200
201        return new_stream
202
203    def _extract_includes(self, stream):
204        streams = [[]] # stacked lists of events of the "compiled" template
205        prefixes = {}
206        fallbacks = []
207        includes = []
208        xinclude_ns = Namespace(self.XINCLUDE_NAMESPACE)
209
210        for kind, data, pos in stream:
211            stream = streams[-1]
212
213            if kind is START:
214                # Record any directive attributes in start tags
215                tag, attrs = data
216                if tag in xinclude_ns:
217                    if tag.localname == 'include':
218                        include_href = attrs.get('href')
219                        if not include_href:
220                            raise TemplateSyntaxError('Include misses required '
221                                                      'attribute "href"',
222                                                      self.filepath, *pos[1:])
223                        includes.append((include_href, attrs.get('parse')))
224                        streams.append([])
225                    elif tag.localname == 'fallback':
226                        streams.append([])
227                        fallbacks.append(streams[-1])
228                else:
229                    stream.append((kind, (tag, attrs), pos))
230
231            elif kind is END:
232                if fallbacks and data == xinclude_ns['fallback']:
233                    fallback_stream = streams.pop()
234                    assert fallback_stream is fallbacks[-1]
235                elif data == xinclude_ns['include']:
236                    fallback = None
237                    if len(fallbacks) == len(includes):
238                        fallback = fallbacks.pop()
239                    streams.pop() # discard anything between the include tags
240                                  # and the fallback element
241                    stream = streams[-1]
242                    href, parse = includes.pop()
243                    try:
244                        cls = {
245                            'xml': MarkupTemplate,
246                            'text': NewTextTemplate
247                        }.get(parse) or self.__class__
248                    except KeyError:
249                        raise TemplateSyntaxError('Invalid value for "parse" '
250                                                  'attribute of include',
251                                                  self.filepath, *pos[1:])
252                    stream.append((INCLUDE, (href, cls, fallback), pos))
253                else:
254                    stream.append((kind, data, pos))
255
256            elif kind is START_NS and data[1] == xinclude_ns:
257                # Strip out the XInclude namespace
258                prefixes[data[0]] = data[1]
259
260            elif kind is END_NS and data in prefixes:
261                prefixes.pop(data)
262
263            else:
264                stream.append((kind, data, pos))
265
266        assert len(streams) == 1
267        return streams[0]
268
269    def _interpolate_attrs(self, stream):
270        for kind, data, pos in stream:
271
272            if kind is START:
273                # Record any directive attributes in start tags
274                tag, attrs = data
275                new_attrs = []
276                for name, value in attrs:
277                    if value:
278                        value = list(interpolate(value, self.filepath, pos[1],
279                                                 pos[2], lookup=self.lookup))
280                        if len(value) == 1 and value[0][0] is TEXT:
281                            value = value[0][1]
282                    new_attrs.append((name, value))
283                data = tag, Attrs(new_attrs)
284
285            yield kind, data, pos
286
287    def _prepare(self, stream, inlined=None):
288        return Template._prepare(
289            self, self._extract_includes(self._interpolate_attrs(stream)),
290            inlined=inlined)
291
292    def add_directives(self, namespace, factory):
293        """Register a custom `DirectiveFactory` for a given namespace.
294       
295        :param namespace: the namespace URI
296        :type namespace: `basestring`
297        :param factory: the directive factory to register
298        :type factory: `DirectiveFactory`
299        :since: version 0.6
300        """
301        assert not self._prepared, 'Too late for adding directives, ' \
302                                   'template already prepared'
303        self._stream = self._extract_directives(self._stream, namespace,
304                                                factory)
305
306    def _match(self, stream, ctxt, start=0, end=None, **vars):
307        """Internal stream filter that applies any defined match templates
308        to the stream.
309        """
310        match_templates = ctxt._match_templates
311
312        def _strip(stream, append):
313            depth = 1
314            next = stream.next
315            while 1:
316                event = next()
317                if event[0] is START:
318                    depth += 1
319                elif event[0] is END:
320                    depth -= 1
321                if depth > 0:
322                    yield event
323                else:
324                    append(event)
325                    break
326
327        for event in stream:
328
329            # We (currently) only care about start and end events for matching
330            # We might care about namespace events in the future, though
331            if not match_templates or (event[0] is not START and
332                                       event[0] is not END):
333                yield event
334                continue
335
336            for idx, (test, path, template, hints, namespaces, directives) \
337                    in enumerate(match_templates):
338                if idx < start or end is not None and idx >= end:
339                    continue
340
341                if test(event, namespaces, ctxt) is True:
342                    if 'match_once' in hints:
343                        del match_templates[idx]
344                        idx -= 1
345
346                    # Let the remaining match templates know about the event so
347                    # they get a chance to update their internal state
348                    for test in [mt[0] for mt in match_templates[idx + 1:]]:
349                        test(event, namespaces, ctxt, updateonly=True)
350
351                    # Consume and store all events until an end event
352                    # corresponding to this start event is encountered
353                    pre_end = idx + 1
354                    if 'match_once' not in hints and 'not_recursive' in hints:
355                        pre_end -= 1
356                    tail = []
357                    inner = _strip(stream, tail.append)
358                    if pre_end > 0:
359                        inner = self._match(inner, ctxt, start=start,
360                                            end=pre_end, **vars)
361                    content = self._include(chain([event], inner, tail), ctxt)
362                    if 'not_buffered' not in hints:
363                        content = list(content)
364                    content = Stream(content)
365
366                    # Make the select() function available in the body of the
367                    # match template
368                    selected = [False]
369                    def select(path):
370                        selected[0] = True
371                        return content.select(path, namespaces, ctxt)
372                    vars = dict(select=select)
373
374                    # Recursively process the output
375                    template = _apply_directives(template, directives, ctxt,
376                                                 vars)
377                    for event in self._match(self._flatten(template, ctxt,
378                                                           **vars),
379                                             ctxt, start=idx + 1, **vars):
380                        yield event
381
382                    # If the match template did not actually call select to
383                    # consume the matched stream, the original events need to
384                    # be consumed here or they'll get appended to the output
385                    if not selected[0]:
386                        for event in content:
387                            pass
388
389                    # Let this match template and the remaining match
390                    # templates know about the last event in the
391                    # matched content, so they can update their
392                    # internal state accordingly
393                    for test in [mt[0] for mt in match_templates[idx:]]:
394                        test(tail[0], namespaces, ctxt, updateonly=True)
395
396                    break
397
398            else: # no matches
399                yield event
Note: See TracBrowser for help on using the repository browser.