Edgewall Software

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

Last change on this file was 1012, checked in by cmlenz, 15 years ago

Ported [1011] to 0.5.x branch.

  • Property svn:eol-style set to native
File size: 13.3 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2008 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 = Namespace('http://genshi.edgewall.org/')
46    XINCLUDE_NAMESPACE = 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_filters(self):
64        Template._init_filters(self)
65        # Make sure the include filter comes after the match filter
66        if self.loader:
67            self.filters.remove(self._include)
68        self.filters += [self._match]
69        if self.loader:
70            self.filters.append(self._include)
71
72    def _parse(self, source, encoding):
73        streams = [[]] # stacked lists of events of the "compiled" template
74        dirmap = {} # temporary mapping of directives to elements
75        ns_prefix = {}
76        depth = 0
77        fallbacks = []
78        includes = []
79
80        if not isinstance(source, Stream):
81            source = XMLParser(source, filename=self.filename,
82                               encoding=encoding)
83
84        for kind, data, pos in source:
85            stream = streams[-1]
86
87            if kind is START_NS:
88                # Strip out the namespace declaration for template directives
89                prefix, uri = data
90                ns_prefix[prefix] = uri
91                if uri not in (self.DIRECTIVE_NAMESPACE,
92                               self.XINCLUDE_NAMESPACE):
93                    stream.append((kind, data, pos))
94
95            elif kind is END_NS:
96                uri = ns_prefix.pop(data, None)
97                if uri and uri not in (self.DIRECTIVE_NAMESPACE,
98                                       self.XINCLUDE_NAMESPACE):
99                    stream.append((kind, data, pos))
100
101            elif kind is START:
102                # Record any directive attributes in start tags
103                tag, attrs = data
104                directives = []
105                strip = False
106
107                if tag in self.DIRECTIVE_NAMESPACE:
108                    cls = self._dir_by_name.get(tag.localname)
109                    if cls is None:
110                        raise BadDirectiveError(tag.localname, self.filepath,
111                                                pos[1])
112                    args = dict([(name.localname, value) for name, value
113                                 in attrs if not name.namespace])
114                    directives.append((cls, args, ns_prefix.copy(), pos))
115                    strip = True
116
117                new_attrs = []
118                for name, value in attrs:
119                    if name in self.DIRECTIVE_NAMESPACE:
120                        cls = self._dir_by_name.get(name.localname)
121                        if cls is None:
122                            raise BadDirectiveError(name.localname,
123                                                    self.filepath, pos[1])
124                        directives.append((cls, value, ns_prefix.copy(), pos))
125                    else:
126                        if value:
127                            value = list(interpolate(value, self.filepath,
128                                                     pos[1], pos[2],
129                                                     lookup=self.lookup))
130                            if len(value) == 1 and value[0][0] is TEXT:
131                                value = value[0][1]
132                        else:
133                            value = [(TEXT, u'', pos)]
134                        new_attrs.append((name, value))
135                new_attrs = Attrs(new_attrs)
136
137                if directives:
138                    index = self._dir_order.index
139                    directives.sort(lambda a, b: cmp(index(a[0]), index(b[0])))
140                    dirmap[(depth, tag)] = (directives, len(stream), strip)
141
142                if tag in self.XINCLUDE_NAMESPACE:
143                    if tag.localname == 'include':
144                        include_href = new_attrs.get('href')
145                        if not include_href:
146                            raise TemplateSyntaxError('Include misses required '
147                                                      'attribute "href"',
148                                                      self.filepath, *pos[1:])
149                        includes.append((include_href, new_attrs.get('parse')))
150                        streams.append([])
151                    elif tag.localname == 'fallback':
152                        streams.append([])
153                        fallbacks.append(streams[-1])
154
155                else:
156                    stream.append((kind, (tag, new_attrs), pos))
157
158                depth += 1
159
160            elif kind is END:
161                depth -= 1
162
163                if fallbacks and data == self.XINCLUDE_NAMESPACE['fallback']:
164                    assert streams.pop() is fallbacks[-1]
165                elif data == self.XINCLUDE_NAMESPACE['include']:
166                    fallback = None
167                    if len(fallbacks) == len(includes):
168                        fallback = fallbacks.pop()
169                    streams.pop() # discard anything between the include tags
170                                  # and the fallback element
171                    stream = streams[-1]
172                    href, parse = includes.pop()
173                    try:
174                        cls = {
175                            'xml': MarkupTemplate,
176                            'text': NewTextTemplate
177                        }[parse or 'xml']
178                    except KeyError:
179                        raise TemplateSyntaxError('Invalid value for "parse" '
180                                                  'attribute of include',
181                                                  self.filepath, *pos[1:])
182                    stream.append((INCLUDE, (href, cls, fallback), pos))
183                else:
184                    stream.append((kind, data, pos))
185
186                # If there have have directive attributes with the corresponding
187                # start tag, move the events inbetween into a "subprogram"
188                if (depth, data) in dirmap:
189                    directives, start_offset, strip = dirmap.pop((depth, data))
190                    substream = stream[start_offset:]
191                    if strip:
192                        substream = substream[1:-1]
193                    stream[start_offset:] = [(SUB, (directives, substream),
194                                              pos)]
195
196            elif kind is PI and data[0] == 'python':
197                if not self.allow_exec:
198                    raise TemplateSyntaxError('Python code blocks not allowed',
199                                              self.filepath, *pos[1:])
200                try:
201                    suite = Suite(data[1], self.filepath, pos[1],
202                                  lookup=self.lookup)
203                except SyntaxError, err:
204                    raise TemplateSyntaxError(err, self.filepath,
205                                              pos[1] + (err.lineno or 1) - 1,
206                                              pos[2] + (err.offset or 0))
207                stream.append((EXEC, suite, pos))
208
209            elif kind is TEXT:
210                for kind, data, pos in interpolate(data, self.filepath, pos[1],
211                                                   pos[2], lookup=self.lookup):
212                    stream.append((kind, data, pos))
213
214            elif kind is COMMENT:
215                if not data.lstrip().startswith('!'):
216                    stream.append((kind, data, pos))
217
218            else:
219                stream.append((kind, data, pos))
220
221        assert len(streams) == 1
222        return streams[0]
223
224    def _match(self, stream, ctxt, start=0, end=None, **vars):
225        """Internal stream filter that applies any defined match templates
226        to the stream.
227        """
228        match_templates = ctxt._match_templates
229
230        tail = []
231        def _strip(stream):
232            depth = 1
233            while 1:
234                event = stream.next()
235                if event[0] is START:
236                    depth += 1
237                elif event[0] is END:
238                    depth -= 1
239                if depth > 0:
240                    yield event
241                else:
242                    tail[:] = [event]
243                    break
244
245        for event in stream:
246
247            # We (currently) only care about start and end events for matching
248            # We might care about namespace events in the future, though
249            if not match_templates or (event[0] is not START and
250                                       event[0] is not END):
251                yield event
252                continue
253
254            for idx, (test, path, template, hints, namespaces, directives) \
255                    in enumerate(match_templates):
256                if idx < start or end is not None and idx >= end:
257                    continue
258
259                if test(event, namespaces, ctxt) is True:
260                    if 'match_once' in hints:
261                        del match_templates[idx]
262                        idx -= 1
263
264                    # Let the remaining match templates know about the event so
265                    # they get a chance to update their internal state
266                    for test in [mt[0] for mt in match_templates[idx + 1:]]:
267                        test(event, namespaces, ctxt, updateonly=True)
268
269                    # Consume and store all events until an end event
270                    # corresponding to this start event is encountered
271                    pre_end = idx + 1
272                    if 'match_once' not in hints and 'not_recursive' in hints:
273                        pre_end -= 1
274                    inner = _strip(stream)
275                    if pre_end > 0:
276                        inner = self._match(inner, ctxt, end=pre_end)
277                    content = self._include(chain([event], inner, tail), ctxt)
278                    if 'not_buffered' not in hints:
279                        content = list(content)
280
281                    # Make the select() function available in the body of the
282                    # match template
283                    selected = [False]
284                    def select(path):
285                        selected[0] = True
286                        return Stream(content).select(path, namespaces, ctxt)
287                    vars = dict(select=select)
288
289                    # Recursively process the output
290                    template = _apply_directives(template, directives, ctxt,
291                                                 **vars)
292                    for event in self._match(
293                            self._exec(
294                                self._eval(
295                                    self._flatten(template, ctxt, **vars),
296                                    ctxt, **vars),
297                                ctxt, **vars),
298                            ctxt, start=idx + 1, **vars):
299                        yield event
300
301                    # If the match template did not actually call select to
302                    # consume the matched stream, the original events need to
303                    # be consumed here or they'll get appended to the output
304                    if not selected[0]:
305                        for event in content:
306                            pass
307
308                    # Let the remaining match templates know about the last
309                    # event in the matched content, so they can update their
310                    # internal state accordingly
311                    for test in [mt[0] for mt in match_templates]:
312                        test(tail[0], namespaces, ctxt, updateonly=True)
313
314                    break
315
316            else: # no matches
317                yield event
Note: See TracBrowser for help on using the repository browser.