cmlenz@336: # -*- coding: utf-8 -*- cmlenz@336: # cmlenz@408: # Copyright (C) 2006-2007 Edgewall Software cmlenz@336: # All rights reserved. cmlenz@336: # cmlenz@336: # This software is licensed as described in the file COPYING, which cmlenz@336: # you should have received as part of this distribution. The terms cmlenz@336: # are also available at http://genshi.edgewall.org/wiki/License. cmlenz@336: # cmlenz@336: # This software consists of voluntary contributions made by many cmlenz@336: # individuals. For the exact contribution history, see the revision cmlenz@336: # history and logs, available at http://genshi.edgewall.org/log/. cmlenz@336: cmlenz@336: """Markup templating engine.""" cmlenz@336: cmlenz@336: from itertools import chain cmlenz@336: cmlenz@636: from genshi.core import Attrs, Markup, Namespace, Stream, StreamEventKind cmlenz@405: from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT cmlenz@336: from genshi.input import XMLParser cmlenz@400: from genshi.template.base import BadDirectiveError, Template, \ cmlenz@475: TemplateSyntaxError, _apply_directives, \ cmlenz@609: EXEC, INCLUDE, SUB cmlenz@405: from genshi.template.eval import Suite cmlenz@407: from genshi.template.interpolation import interpolate cmlenz@336: from genshi.template.directives import * cmlenz@610: from genshi.template.text import NewTextTemplate cmlenz@336: cmlenz@425: __all__ = ['MarkupTemplate'] cmlenz@425: __docformat__ = 'restructuredtext en' cmlenz@425: cmlenz@336: cmlenz@336: class MarkupTemplate(Template): cmlenz@336: """Implementation of the template language for XML-based templates. cmlenz@336: cmlenz@336: >>> tmpl = MarkupTemplate('''''') cmlenz@336: >>> print tmpl.generate(items=[1, 2, 3]) cmlenz@336: cmlenz@336: """ cmlenz@427: cmlenz@363: DIRECTIVE_NAMESPACE = Namespace('http://genshi.edgewall.org/') cmlenz@363: XINCLUDE_NAMESPACE = Namespace('http://www.w3.org/2001/XInclude') cmlenz@336: cmlenz@336: directives = [('def', DefDirective), cmlenz@336: ('match', MatchDirective), cmlenz@336: ('when', WhenDirective), cmlenz@336: ('otherwise', OtherwiseDirective), cmlenz@336: ('for', ForDirective), cmlenz@336: ('if', IfDirective), cmlenz@336: ('choose', ChooseDirective), cmlenz@336: ('with', WithDirective), cmlenz@336: ('replace', ReplaceDirective), cmlenz@336: ('content', ContentDirective), cmlenz@336: ('attrs', AttrsDirective), cmlenz@336: ('strip', StripDirective)] cmlenz@605: serializer = 'xml' cmlenz@636: _number_conv = Markup cmlenz@336: cmlenz@336: def __init__(self, source, basedir=None, filename=None, loader=None, aronacher@642: encoding=None, lookup='strict', allow_exec=True, aronacher@647: restricted=False): cmlenz@336: Template.__init__(self, source, basedir=basedir, filename=filename, cmlenz@545: loader=loader, encoding=encoding, lookup=lookup, aronacher@647: allow_exec=allow_exec, restricted=restricted) cmlenz@496: # Make sure the include filter comes after the match filter cmlenz@496: if loader: cmlenz@496: self.filters.remove(self._include) cmlenz@609: self.filters += [self._match] cmlenz@496: if loader: cmlenz@496: self.filters.append(self._include) cmlenz@336: cmlenz@374: def _parse(self, source, encoding): cmlenz@381: streams = [[]] # stacked lists of events of the "compiled" template cmlenz@336: dirmap = {} # temporary mapping of directives to elements cmlenz@336: ns_prefix = {} cmlenz@336: depth = 0 cmlenz@590: fallbacks = [] cmlenz@590: includes = [] cmlenz@336: cmlenz@374: if not isinstance(source, Stream): cmlenz@434: source = XMLParser(source, filename=self.filename, cmlenz@374: encoding=encoding) cmlenz@374: cmlenz@374: for kind, data, pos in source: cmlenz@381: stream = streams[-1] cmlenz@336: cmlenz@336: if kind is START_NS: cmlenz@336: # Strip out the namespace declaration for template directives cmlenz@336: prefix, uri = data cmlenz@336: ns_prefix[prefix] = uri cmlenz@363: if uri not in (self.DIRECTIVE_NAMESPACE, cmlenz@363: self.XINCLUDE_NAMESPACE): cmlenz@336: stream.append((kind, data, pos)) cmlenz@336: cmlenz@336: elif kind is END_NS: cmlenz@336: uri = ns_prefix.pop(data, None) cmlenz@363: if uri and uri not in (self.DIRECTIVE_NAMESPACE, cmlenz@363: self.XINCLUDE_NAMESPACE): cmlenz@336: stream.append((kind, data, pos)) cmlenz@336: cmlenz@336: elif kind is START: cmlenz@336: # Record any directive attributes in start tags cmlenz@363: tag, attrs = data cmlenz@336: directives = [] cmlenz@336: strip = False cmlenz@336: cmlenz@363: if tag in self.DIRECTIVE_NAMESPACE: cmlenz@336: cls = self._dir_by_name.get(tag.localname) cmlenz@336: if cls is None: cmlenz@336: raise BadDirectiveError(tag.localname, self.filepath, cmlenz@336: pos[1]) cmlenz@552: args = dict([(name.localname, value) for name, value cmlenz@552: in attrs if not name.namespace]) cmlenz@552: directives.append((cls, args, ns_prefix.copy(), pos)) cmlenz@336: strip = True cmlenz@336: cmlenz@363: new_attrs = [] cmlenz@363: for name, value in attrs: cmlenz@363: if name in self.DIRECTIVE_NAMESPACE: cmlenz@336: cls = self._dir_by_name.get(name.localname) cmlenz@336: if cls is None: cmlenz@336: raise BadDirectiveError(name.localname, cmlenz@336: self.filepath, pos[1]) cmlenz@362: directives.append((cls, value, ns_prefix.copy(), pos)) cmlenz@336: else: cmlenz@336: if value: cmlenz@442: value = list(interpolate(value, self.basedir, cmlenz@442: pos[0], pos[1], pos[2], aronacher@647: lookup=self.lookup, aronacher@647: restricted=self.restricted)) cmlenz@336: if len(value) == 1 and value[0][0] is TEXT: cmlenz@336: value = value[0][1] cmlenz@336: else: cmlenz@336: value = [(TEXT, u'', pos)] cmlenz@363: new_attrs.append((name, value)) cmlenz@363: new_attrs = Attrs(new_attrs) cmlenz@336: cmlenz@336: if directives: cmlenz@336: index = self._dir_order.index cmlenz@362: directives.sort(lambda a, b: cmp(index(a[0]), index(b[0]))) cmlenz@336: dirmap[(depth, tag)] = (directives, len(stream), strip) cmlenz@336: cmlenz@363: if tag in self.XINCLUDE_NAMESPACE: cmlenz@363: if tag.localname == 'include': cmlenz@363: include_href = new_attrs.get('href') cmlenz@363: if not include_href: cmlenz@363: raise TemplateSyntaxError('Include misses required ' cmlenz@422: 'attribute "href"', cmlenz@422: self.filepath, *pos[1:]) cmlenz@610: includes.append((include_href, new_attrs.get('parse'))) cmlenz@381: streams.append([]) cmlenz@363: elif tag.localname == 'fallback': cmlenz@590: streams.append([]) cmlenz@590: fallbacks.append(streams[-1]) cmlenz@363: cmlenz@363: else: cmlenz@363: stream.append((kind, (tag, new_attrs), pos)) cmlenz@363: cmlenz@336: depth += 1 cmlenz@336: cmlenz@336: elif kind is END: cmlenz@336: depth -= 1 cmlenz@363: cmlenz@590: if fallbacks and data == self.XINCLUDE_NAMESPACE['fallback']: cmlenz@590: assert streams.pop() is fallbacks[-1] cmlenz@363: elif data == self.XINCLUDE_NAMESPACE['include']: cmlenz@590: fallback = None cmlenz@590: if len(fallbacks) == len(includes): cmlenz@590: fallback = fallbacks.pop() cmlenz@590: streams.pop() # discard anything between the include tags cmlenz@590: # and the fallback element cmlenz@381: stream = streams[-1] cmlenz@610: href, parse = includes.pop() cmlenz@610: try: cmlenz@610: cls = { cmlenz@610: 'xml': MarkupTemplate, cmlenz@610: 'text': NewTextTemplate cmlenz@610: }[parse or 'xml'] cmlenz@610: except KeyError: cmlenz@610: raise TemplateSyntaxError('Invalid value for "parse" ' cmlenz@610: 'attribute of include', cmlenz@610: self.filepath, *pos[1:]) cmlenz@610: stream.append((INCLUDE, (href, cls, fallback), pos)) cmlenz@363: else: cmlenz@363: stream.append((kind, data, pos)) cmlenz@336: cmlenz@336: # If there have have directive attributes with the corresponding cmlenz@336: # start tag, move the events inbetween into a "subprogram" cmlenz@336: if (depth, data) in dirmap: cmlenz@336: directives, start_offset, strip = dirmap.pop((depth, data)) cmlenz@336: substream = stream[start_offset:] cmlenz@336: if strip: cmlenz@336: substream = substream[1:-1] cmlenz@336: stream[start_offset:] = [(SUB, (directives, substream), cmlenz@336: pos)] cmlenz@336: cmlenz@405: elif kind is PI and data[0] == 'python': cmlenz@545: if not self.allow_exec: cmlenz@545: raise TemplateSyntaxError('Python code blocks not allowed', cmlenz@545: self.filepath, *pos[1:]) cmlenz@405: try: cmlenz@601: suite = Suite(data[1], self.filepath, pos[1], cmlenz@442: lookup=self.lookup) cmlenz@405: except SyntaxError, err: cmlenz@405: raise TemplateSyntaxError(err, self.filepath, cmlenz@405: pos[1] + (err.lineno or 1) - 1, cmlenz@405: pos[2] + (err.offset or 0)) cmlenz@405: stream.append((EXEC, suite, pos)) cmlenz@405: cmlenz@336: elif kind is TEXT: cmlenz@442: for kind, data, pos in interpolate(data, self.basedir, pos[0], cmlenz@442: pos[1], pos[2], aronacher@647: lookup=self.lookup, aronacher@647: restricted=self.restricted): cmlenz@336: stream.append((kind, data, pos)) cmlenz@336: cmlenz@336: elif kind is COMMENT: cmlenz@336: if not data.lstrip().startswith('!'): cmlenz@336: stream.append((kind, data, pos)) cmlenz@336: cmlenz@336: else: cmlenz@336: stream.append((kind, data, pos)) cmlenz@336: cmlenz@381: assert len(streams) == 1 cmlenz@381: return streams[0] cmlenz@381: cmlenz@336: def _match(self, stream, ctxt, match_templates=None): cmlenz@336: """Internal stream filter that applies any defined match templates cmlenz@336: to the stream. cmlenz@336: """ cmlenz@336: if match_templates is None: cmlenz@336: match_templates = ctxt._match_templates cmlenz@336: cmlenz@336: tail = [] cmlenz@336: def _strip(stream): cmlenz@336: depth = 1 cmlenz@336: while 1: cmlenz@336: event = stream.next() cmlenz@336: if event[0] is START: cmlenz@336: depth += 1 cmlenz@336: elif event[0] is END: cmlenz@336: depth -= 1 cmlenz@336: if depth > 0: cmlenz@336: yield event cmlenz@336: else: cmlenz@336: tail[:] = [event] cmlenz@336: break cmlenz@336: cmlenz@336: for event in stream: cmlenz@336: cmlenz@336: # We (currently) only care about start and end events for matching cmlenz@336: # We might care about namespace events in the future, though cmlenz@336: if not match_templates or (event[0] is not START and cmlenz@336: event[0] is not END): cmlenz@336: yield event cmlenz@336: continue cmlenz@336: cmlenz@602: for idx, (test, path, template, hints, namespaces, directives) \ cmlenz@602: in enumerate(match_templates): cmlenz@336: cmlenz@336: if test(event, namespaces, ctxt) is True: cmlenz@602: if 'match_once' in hints: cmlenz@602: del match_templates[idx] cmlenz@602: idx -= 1 cmlenz@336: cmlenz@336: # Let the remaining match templates know about the event so cmlenz@336: # they get a chance to update their internal state cmlenz@336: for test in [mt[0] for mt in match_templates[idx + 1:]]: cmlenz@336: test(event, namespaces, ctxt, updateonly=True) cmlenz@336: cmlenz@336: # Consume and store all events until an end event cmlenz@336: # corresponding to this start event is encountered cmlenz@602: inner = _strip(stream) cmlenz@602: if 'match_once' not in hints \ cmlenz@602: and 'not_recursive' not in hints: cmlenz@602: inner = self._match(inner, ctxt, [match_templates[idx]]) cmlenz@602: content = list(self._include(chain([event], inner, tail), cmlenz@602: ctxt)) cmlenz@336: cmlenz@336: for test in [mt[0] for mt in match_templates]: cmlenz@336: test(tail[0], namespaces, ctxt, updateonly=True) cmlenz@336: cmlenz@336: # Make the select() function available in the body of the cmlenz@336: # match template cmlenz@336: def select(path): cmlenz@336: return Stream(content).select(path, namespaces, ctxt) cmlenz@336: ctxt.push(dict(select=select)) cmlenz@336: cmlenz@336: # Recursively process the output cmlenz@336: template = _apply_directives(template, ctxt, directives) cmlenz@602: remaining = match_templates cmlenz@602: if 'match_once' not in hints: cmlenz@602: remaining = remaining[:idx] + remaining[idx + 1:] cmlenz@336: for event in self._match(self._eval(self._flatten(template, cmlenz@336: ctxt), cmlenz@602: ctxt), ctxt, remaining): cmlenz@336: yield event cmlenz@336: cmlenz@336: ctxt.pop() cmlenz@336: break cmlenz@336: cmlenz@336: else: # no matches cmlenz@336: yield event