1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2006-2007 Edgewall Software
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.
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/.
14
"""Markup templating engine."""
16
from itertools import chain
18
from textwrap import dedent
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 *
30
if sys.version_info < (2, 4):
31
_ctxt2dict = lambda ctxt: ctxt.frames[0]
33
_ctxt2dict = lambda ctxt: ctxt
35
__all__ = ['MarkupTemplate']
36
__docformat__ = 'restructuredtext en'
39
class MarkupTemplate(Template):
40
"""Implementation of the template language for XML-based templates.
42
>>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
43
... <li py:for="item in items">${item}</li>
45
>>> print tmpl.generate(items=[1, 2, 3])
47
<li>1</li><li>2</li><li>3</li>
50
EXEC = StreamEventKind('EXEC')
51
"""Stream event kind representing a Python code suite to execute."""
53
INCLUDE = StreamEventKind('INCLUDE')
54
"""Stream event kind representing the inclusion of another template."""
56
DIRECTIVE_NAMESPACE = Namespace('http://genshi.edgewall.org/')
57
XINCLUDE_NAMESPACE = Namespace('http://www.w3.org/2001/XInclude')
59
directives = [('def', DefDirective),
60
('match', MatchDirective),
61
('when', WhenDirective),
62
('otherwise', OtherwiseDirective),
63
('for', ForDirective),
65
('choose', ChooseDirective),
66
('with', WithDirective),
67
('replace', ReplaceDirective),
68
('content', ContentDirective),
69
('attrs', AttrsDirective),
70
('strip', StripDirective)]
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)
77
self.filters += [self._exec, self._match]
79
self.filters.append(self._include)
81
def _parse(self, source, encoding):
82
streams = [[]] # stacked lists of events of the "compiled" template
83
dirmap = {} # temporary mapping of directives to elements
89
if not isinstance(source, Stream):
90
source = XMLParser(source, filename=self.filename,
93
for kind, data, pos in source:
97
# Strip out the namespace declaration for template directives
99
ns_prefix[prefix] = uri
100
if uri not in (self.DIRECTIVE_NAMESPACE,
101
self.XINCLUDE_NAMESPACE):
102
stream.append((kind, data, pos))
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))
111
# Record any directive attributes in start tags
116
if tag in self.DIRECTIVE_NAMESPACE:
117
cls = self._dir_by_name.get(tag.localname)
119
raise BadDirectiveError(tag.localname, self.filepath,
121
value = attrs.get(getattr(cls, 'ATTRIBUTE', None), '')
122
directives.append((cls, value, ns_prefix.copy(), pos))
126
for name, value in attrs:
127
if name in self.DIRECTIVE_NAMESPACE:
128
cls = self._dir_by_name.get(name.localname)
130
raise BadDirectiveError(name.localname,
131
self.filepath, pos[1])
132
directives.append((cls, value, ns_prefix.copy(), pos))
135
value = list(interpolate(value, self.basedir,
136
pos[0], pos[1], pos[2],
138
if len(value) == 1 and value[0][0] is TEXT:
141
value = [(TEXT, u'', pos)]
142
new_attrs.append((name, value))
143
new_attrs = Attrs(new_attrs)
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)
150
if tag in self.XINCLUDE_NAMESPACE:
151
if tag.localname == 'include':
152
include_href = new_attrs.get('href')
154
raise TemplateSyntaxError('Include misses required '
156
self.filepath, *pos[1:])
158
elif tag.localname == 'fallback':
162
stream.append((kind, (tag, new_attrs), pos))
169
if in_fallback and data == self.XINCLUDE_NAMESPACE['fallback']:
171
elif data == self.XINCLUDE_NAMESPACE['include']:
172
fallback = streams.pop()
174
stream.append((INCLUDE, (include_href, fallback), pos))
176
stream.append((kind, data, pos))
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:]
184
substream = substream[1:-1]
185
stream[start_offset:] = [(SUB, (directives, substream),
188
elif kind is PI and data[0] == 'python':
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
195
lines = [line.expandtabs() for line in data[1].splitlines()]
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],
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))
211
for kind, data, pos in interpolate(data, self.basedir, pos[0],
214
stream.append((kind, data, pos))
216
elif kind is COMMENT:
217
if not data.lstrip().startswith('!'):
218
stream.append((kind, data, pos))
221
stream.append((kind, data, pos))
223
assert len(streams) == 1
226
def _prepare(self, stream):
227
for kind, data, pos in Template._prepare(self, stream):
229
data = data[0], list(self._prepare(data[1]))
230
yield kind, data, pos
232
def _exec(self, stream, ctxt):
233
"""Internal stream filter that executes code in ``<?python ?>``
234
processing instructions.
238
event[1].execute(_ctxt2dict(ctxt))
242
def _include(self, stream, ctxt):
243
"""Internal stream filter that performs inclusion of external
247
if event[0] is INCLUDE:
248
href, fallback = event[1]
249
if not isinstance(href, basestring):
251
for subkind, subdata, subpos in self._eval(href, ctxt):
253
parts.append(subdata)
254
href = u''.join([x for x in parts if x is not None])
256
tmpl = self.loader.load(href, relative_to=event[2][0])
257
for event in tmpl.generate(ctxt):
259
except TemplateNotFound:
262
for filter_ in self.filters:
263
fallback = filter_(iter(fallback), ctxt)
264
for event in fallback:
269
def _match(self, stream, ctxt, match_templates=None):
270
"""Internal stream filter that applies any defined match templates
273
if match_templates is None:
274
match_templates = ctxt._match_templates
280
event = stream.next()
281
if event[0] is START:
283
elif event[0] is END:
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):
300
for idx, (test, path, template, namespaces, directives) in \
301
enumerate(match_templates):
303
if test(event, namespaces, ctxt) is True:
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)
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]]),
316
content = list(self._include(content, ctxt))
318
for test in [mt[0] for mt in match_templates]:
319
test(tail[0], namespaces, ctxt, updateonly=True)
321
# Make the select() function available in the body of the
324
return Stream(content).select(path, namespaces, ctxt)
325
ctxt.push(dict(select=select))
327
# Recursively process the output
328
template = _apply_directives(template, ctxt, directives)
329
for event in self._match(self._eval(self._flatten(template,
332
match_templates[:idx] +
333
match_templates[idx + 1:]):
343
EXEC = MarkupTemplate.EXEC
344
INCLUDE = MarkupTemplate.INCLUDE