| 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 | 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 | |
|---|
| 349 | EXEC = MarkupTemplate.EXEC |
|---|
| 350 | INCLUDE = MarkupTemplate.INCLUDE |
|---|