~ubuntu-branches/ubuntu/raring/genshi/raring-proposed

« back to all changes in this revision

Viewing changes to genshi/template/markup.py

  • Committer: Bazaar Package Importer
  • Author(s): Arnaud Fontaine
  • Date: 2007-04-16 17:49:03 UTC
  • mfrom: (1.1.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20070416174903-x2p3n9g890v18d0m
Tags: 0.4-1
* New upstream release.
* Remove useless python-markup transition package.
* Add Provides against python-markup.
* Add doc-base.
* Add depends against python-xml.
* Add suggests to python-setuptools.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
 
16
from itertools import chain
 
17
import sys
 
18
from textwrap import dedent
 
19
 
 
20
from genshi.core import Attrs, Namespace, Stream, StreamEventKind
 
21
from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT
 
22
from genshi.input import XMLParser
 
23
from genshi.template.base import BadDirectiveError, Template, \
 
24
                                 TemplateSyntaxError, _apply_directives, SUB
 
25
from genshi.template.eval import Suite
 
26
from genshi.template.interpolation import interpolate
 
27
from genshi.template.loader import TemplateNotFound
 
28
from genshi.template.directives import *
 
29
 
 
30
if sys.version_info < (2, 4):
 
31
    _ctxt2dict = lambda ctxt: ctxt.frames[0]
 
32
else:
 
33
    _ctxt2dict = lambda ctxt: ctxt
 
34
 
 
35
__all__ = ['MarkupTemplate']
 
36
__docformat__ = 'restructuredtext en'
 
37
 
 
38
 
 
39
class 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
        in_fallback = 0
 
87
        include_href = None
 
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
                        streams.append([])
 
158
                    elif tag.localname == 'fallback':
 
159
                        in_fallback += 1
 
160
 
 
161
                else:
 
162
                    stream.append((kind, (tag, new_attrs), pos))
 
163
 
 
164
                depth += 1
 
165
 
 
166
            elif kind is END:
 
167
                depth -= 1
 
168
 
 
169
                if in_fallback and data == self.XINCLUDE_NAMESPACE['fallback']:
 
170
                    in_fallback -= 1
 
171
                elif data == self.XINCLUDE_NAMESPACE['include']:
 
172
                    fallback = streams.pop()
 
173
                    stream = streams[-1]
 
174
                    stream.append((INCLUDE, (include_href, fallback), pos))
 
175
                else:
 
176
                    stream.append((kind, data, pos))
 
177
 
 
178
                # If there have have directive attributes with the corresponding
 
179
                # start tag, move the events inbetween into a "subprogram"
 
180
                if (depth, data) in dirmap:
 
181
                    directives, start_offset, strip = dirmap.pop((depth, data))
 
182
                    substream = stream[start_offset:]
 
183
                    if strip:
 
184
                        substream = substream[1:-1]
 
185
                    stream[start_offset:] = [(SUB, (directives, substream),
 
186
                                              pos)]
 
187
 
 
188
            elif kind is PI and data[0] == 'python':
 
189
                try:
 
190
                    # As Expat doesn't report whitespace between the PI target
 
191
                    # and the data, we have to jump through some hoops here to
 
192
                    # get correctly indented Python code
 
193
                    # Unfortunately, we'll still probably not get the line
 
194
                    # number quite right
 
195
                    lines = [line.expandtabs() for line in data[1].splitlines()]
 
196
                    first = lines[0]
 
197
                    rest = dedent('\n'.join(lines[1:]))
 
198
                    if first.rstrip().endswith(':') and not rest[0].isspace():
 
199
                        rest = '\n'.join(['    ' + line for line
 
200
                                          in rest.splitlines()])
 
201
                    source = '\n'.join([first, rest])
 
202
                    suite = Suite(source, self.filepath, pos[1],
 
203
                                  lookup=self.lookup)
 
204
                except SyntaxError, err:
 
205
                    raise TemplateSyntaxError(err, self.filepath,
 
206
                                              pos[1] + (err.lineno or 1) - 1,
 
207
                                              pos[2] + (err.offset or 0))
 
208
                stream.append((EXEC, suite, pos))
 
209
 
 
210
            elif kind is TEXT:
 
211
                for kind, data, pos in interpolate(data, self.basedir, pos[0],
 
212
                                                   pos[1], pos[2],
 
213
                                                   lookup=self.lookup):
 
214
                    stream.append((kind, data, pos))
 
215
 
 
216
            elif kind is COMMENT:
 
217
                if not data.lstrip().startswith('!'):
 
218
                    stream.append((kind, data, pos))
 
219
 
 
220
            else:
 
221
                stream.append((kind, data, pos))
 
222
 
 
223
        assert len(streams) == 1
 
224
        return streams[0]
 
225
 
 
226
    def _prepare(self, stream):
 
227
        for kind, data, pos in Template._prepare(self, stream):
 
228
            if kind is INCLUDE:
 
229
                data = data[0], list(self._prepare(data[1]))
 
230
            yield kind, data, pos
 
231
 
 
232
    def _exec(self, stream, ctxt):
 
233
        """Internal stream filter that executes code in ``<?python ?>``
 
234
        processing instructions.
 
235
        """
 
236
        for event in stream:
 
237
            if event[0] is EXEC:
 
238
                event[1].execute(_ctxt2dict(ctxt))
 
239
            else:
 
240
                yield event
 
241
 
 
242
    def _include(self, stream, ctxt):
 
243
        """Internal stream filter that performs inclusion of external
 
244
        template files.
 
245
        """
 
246
        for event in stream:
 
247
            if event[0] is INCLUDE:
 
248
                href, fallback = event[1]
 
249
                if not isinstance(href, basestring):
 
250
                    parts = []
 
251
                    for subkind, subdata, subpos in self._eval(href, ctxt):
 
252
                        if subkind is TEXT:
 
253
                            parts.append(subdata)
 
254
                    href = u''.join([x for x in parts if x is not None])
 
255
                try:
 
256
                    tmpl = self.loader.load(href, relative_to=event[2][0])
 
257
                    for event in tmpl.generate(ctxt):
 
258
                        yield event
 
259
                except TemplateNotFound:
 
260
                    if fallback is None:
 
261
                        raise
 
262
                    for filter_ in self.filters:
 
263
                        fallback = filter_(iter(fallback), ctxt)
 
264
                    for event in fallback:
 
265
                        yield event
 
266
            else:
 
267
                yield event
 
268
 
 
269
    def _match(self, stream, ctxt, match_templates=None):
 
270
        """Internal stream filter that applies any defined match templates
 
271
        to the stream.
 
272
        """
 
273
        if match_templates is None:
 
274
            match_templates = ctxt._match_templates
 
275
 
 
276
        tail = []
 
277
        def _strip(stream):
 
278
            depth = 1
 
279
            while 1:
 
280
                event = stream.next()
 
281
                if event[0] is START:
 
282
                    depth += 1
 
283
                elif event[0] is END:
 
284
                    depth -= 1
 
285
                if depth > 0:
 
286
                    yield event
 
287
                else:
 
288
                    tail[:] = [event]
 
289
                    break
 
290
 
 
291
        for event in stream:
 
292
 
 
293
            # We (currently) only care about start and end events for matching
 
294
            # We might care about namespace events in the future, though
 
295
            if not match_templates or (event[0] is not START and
 
296
                                       event[0] is not END):
 
297
                yield event
 
298
                continue
 
299
 
 
300
            for idx, (test, path, template, namespaces, directives) in \
 
301
                    enumerate(match_templates):
 
302
 
 
303
                if test(event, namespaces, ctxt) is True:
 
304
 
 
305
                    # Let the remaining match templates know about the event so
 
306
                    # they get a chance to update their internal state
 
307
                    for test in [mt[0] for mt in match_templates[idx + 1:]]:
 
308
                        test(event, namespaces, ctxt, updateonly=True)
 
309
 
 
310
                    # Consume and store all events until an end event
 
311
                    # corresponding to this start event is encountered
 
312
                    content = chain([event],
 
313
                                    self._match(_strip(stream), ctxt,
 
314
                                                [match_templates[idx]]),
 
315
                                    tail)
 
316
                    content = list(self._include(content, ctxt))
 
317
 
 
318
                    for test in [mt[0] for mt in match_templates]:
 
319
                        test(tail[0], namespaces, ctxt, updateonly=True)
 
320
 
 
321
                    # Make the select() function available in the body of the
 
322
                    # match template
 
323
                    def select(path):
 
324
                        return Stream(content).select(path, namespaces, ctxt)
 
325
                    ctxt.push(dict(select=select))
 
326
 
 
327
                    # Recursively process the output
 
328
                    template = _apply_directives(template, ctxt, directives)
 
329
                    for event in self._match(self._eval(self._flatten(template,
 
330
                                                                      ctxt),
 
331
                                                        ctxt), ctxt,
 
332
                                             match_templates[:idx] +
 
333
                                             match_templates[idx + 1:]):
 
334
                        yield event
 
335
 
 
336
                    ctxt.pop()
 
337
                    break
 
338
 
 
339
            else: # no matches
 
340
                yield event
 
341
 
 
342
 
 
343
EXEC = MarkupTemplate.EXEC
 
344
INCLUDE = MarkupTemplate.INCLUDE