cmlenz@336: # -*- coding: utf-8 -*- cmlenz@336: # cmlenz@820: # Copyright (C) 2006-2008 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@820: from genshi.core import Attrs, Markup, Namespace, Stream, StreamEventKind cmlenz@500: from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT cmlenz@336: from genshi.input import XMLParser cmlenz@500: from genshi.template.base import BadDirectiveError, Template, \ cmlenz@500: TemplateSyntaxError, _apply_directives, \ cmlenz@820: EXEC, INCLUDE, SUB cmlenz@500: from genshi.template.eval import Suite cmlenz@500: from genshi.template.interpolation import interpolate cmlenz@336: from genshi.template.directives import * cmlenz@820: from genshi.template.text import NewTextTemplate cmlenz@500: cmlenz@500: __all__ = ['MarkupTemplate'] cmlenz@500: __docformat__ = 'restructuredtext en' cmlenz@500: 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@395: cmlenz@820: DIRECTIVE_NAMESPACE = 'http://genshi.edgewall.org/' cmlenz@820: XINCLUDE_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@820: serializer = 'xml' cmlenz@820: _number_conv = Markup cmlenz@336: cmlenz@820: def __init__(self, source, filepath=None, filename=None, loader=None, cmlenz@820: encoding=None, lookup='strict', allow_exec=True): cmlenz@820: Template.__init__(self, source, filepath=filepath, filename=filename, cmlenz@820: loader=loader, encoding=encoding, lookup=lookup, cmlenz@820: allow_exec=allow_exec) cmlenz@820: self.add_directives(self.DIRECTIVE_NAMESPACE, self) cmlenz@820: cmlenz@820: def _init_filters(self): cmlenz@820: Template._init_filters(self) cmlenz@500: # Make sure the include filter comes after the match filter cmlenz@820: if self.loader: cmlenz@500: self.filters.remove(self._include) cmlenz@820: self.filters += [self._match] cmlenz@820: if self.loader: cmlenz@395: self.filters.append(self._include) cmlenz@336: cmlenz@395: def _parse(self, source, encoding): cmlenz@395: if not isinstance(source, Stream): cmlenz@395: source = XMLParser(source, filename=self.filename, cmlenz@395: encoding=encoding) cmlenz@820: stream = [] cmlenz@395: cmlenz@395: for kind, data, pos in source: cmlenz@336: cmlenz@820: if kind is TEXT: cmlenz@820: for kind, data, pos in interpolate(data, self.filepath, pos[1], cmlenz@820: pos[2], lookup=self.lookup): cmlenz@336: stream.append((kind, data, pos)) cmlenz@336: cmlenz@500: elif kind is PI and data[0] == 'python': cmlenz@820: if not self.allow_exec: cmlenz@820: raise TemplateSyntaxError('Python code blocks not allowed', cmlenz@820: self.filepath, *pos[1:]) cmlenz@500: try: cmlenz@820: suite = Suite(data[1], self.filepath, pos[1], cmlenz@500: lookup=self.lookup) cmlenz@500: except SyntaxError, err: cmlenz@500: raise TemplateSyntaxError(err, self.filepath, cmlenz@500: pos[1] + (err.lineno or 1) - 1, cmlenz@500: pos[2] + (err.offset or 0)) cmlenz@500: stream.append((EXEC, suite, pos)) cmlenz@500: 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@820: return stream cmlenz@820: cmlenz@820: def _extract_directives(self, stream, namespace, factory): cmlenz@820: depth = 0 cmlenz@820: dirmap = {} # temporary mapping of directives to elements cmlenz@820: new_stream = [] cmlenz@820: ns_prefix = {} # namespace prefixes in use cmlenz@820: cmlenz@820: for kind, data, pos in stream: cmlenz@820: cmlenz@820: if kind is START: cmlenz@820: tag, attrs = data cmlenz@820: directives = [] cmlenz@820: strip = False cmlenz@820: cmlenz@820: if tag.namespace == namespace: cmlenz@820: cls = factory.get_directive(tag.localname) cmlenz@820: if cls is None: cmlenz@820: raise BadDirectiveError(tag.localname, cmlenz@820: self.filepath, pos[1]) cmlenz@820: args = dict([(name.localname, value) for name, value cmlenz@820: in attrs if not name.namespace]) cmlenz@820: directives.append((cls, args, ns_prefix.copy(), pos)) cmlenz@820: strip = True cmlenz@820: cmlenz@820: new_attrs = [] cmlenz@820: for name, value in attrs: cmlenz@820: if name.namespace == namespace: cmlenz@820: cls = factory.get_directive(name.localname) cmlenz@820: if cls is None: cmlenz@820: raise BadDirectiveError(name.localname, cmlenz@820: self.filepath, pos[1]) cmlenz@820: if type(value) is list and len(value) == 1: cmlenz@820: value = value[0][1] cmlenz@820: directives.append((cls, value, ns_prefix.copy(), cmlenz@820: pos)) cmlenz@820: else: cmlenz@820: new_attrs.append((name, value)) cmlenz@820: new_attrs = Attrs(new_attrs) cmlenz@820: cmlenz@820: if directives: cmlenz@820: directives.sort(self.compare_directives()) cmlenz@820: dirmap[(depth, tag)] = (directives, len(new_stream), cmlenz@820: strip) cmlenz@820: cmlenz@820: new_stream.append((kind, (tag, new_attrs), pos)) cmlenz@820: depth += 1 cmlenz@820: cmlenz@820: elif kind is END: cmlenz@820: depth -= 1 cmlenz@820: new_stream.append((kind, data, pos)) cmlenz@820: cmlenz@820: # If there have have directive attributes with the cmlenz@820: # corresponding start tag, move the events inbetween into cmlenz@820: # a "subprogram" cmlenz@820: if (depth, data) in dirmap: cmlenz@820: directives, offset, strip = dirmap.pop((depth, data)) cmlenz@820: substream = new_stream[offset:] cmlenz@820: if strip: cmlenz@820: substream = substream[1:-1] cmlenz@820: new_stream[offset:] = [ cmlenz@820: (SUB, (directives, substream), pos) cmlenz@820: ] cmlenz@820: cmlenz@820: elif kind is SUB: cmlenz@820: directives, substream = data cmlenz@820: substream = self._extract_directives(substream, namespace, cmlenz@820: factory) cmlenz@820: cmlenz@820: if len(substream) == 1 and substream[0][0] is SUB: cmlenz@820: added_directives, substream = substream[0][1] cmlenz@820: directives += added_directives cmlenz@820: cmlenz@820: new_stream.append((kind, (directives, substream), pos)) cmlenz@820: cmlenz@820: elif kind is START_NS: cmlenz@820: # Strip out the namespace declaration for template cmlenz@820: # directives cmlenz@820: prefix, uri = data cmlenz@820: ns_prefix[prefix] = uri cmlenz@820: if uri != namespace: cmlenz@820: new_stream.append((kind, data, pos)) cmlenz@820: cmlenz@820: elif kind is END_NS: cmlenz@820: uri = ns_prefix.pop(data, None) cmlenz@820: if uri and uri != namespace: cmlenz@820: new_stream.append((kind, data, pos)) cmlenz@820: cmlenz@820: else: cmlenz@820: new_stream.append((kind, data, pos)) cmlenz@820: cmlenz@820: return new_stream cmlenz@820: cmlenz@820: def _extract_includes(self, stream): cmlenz@820: streams = [[]] # stacked lists of events of the "compiled" template cmlenz@820: prefixes = {} cmlenz@820: fallbacks = [] cmlenz@820: includes = [] cmlenz@820: xinclude_ns = Namespace(self.XINCLUDE_NAMESPACE) cmlenz@820: cmlenz@820: for kind, data, pos in stream: cmlenz@820: stream = streams[-1] cmlenz@820: cmlenz@820: if kind is START: cmlenz@820: # Record any directive attributes in start tags cmlenz@820: tag, attrs = data cmlenz@820: if tag in xinclude_ns: cmlenz@820: if tag.localname == 'include': cmlenz@820: include_href = attrs.get('href') cmlenz@820: if not include_href: cmlenz@820: raise TemplateSyntaxError('Include misses required ' cmlenz@820: 'attribute "href"', cmlenz@820: self.filepath, *pos[1:]) cmlenz@820: includes.append((include_href, attrs.get('parse'))) cmlenz@820: streams.append([]) cmlenz@820: elif tag.localname == 'fallback': cmlenz@820: streams.append([]) cmlenz@820: fallbacks.append(streams[-1]) cmlenz@820: else: cmlenz@820: stream.append((kind, (tag, attrs), pos)) cmlenz@820: cmlenz@820: elif kind is END: cmlenz@820: if fallbacks and data == xinclude_ns['fallback']: cmlenz@820: assert streams.pop() is fallbacks[-1] cmlenz@820: elif data == xinclude_ns['include']: cmlenz@820: fallback = None cmlenz@820: if len(fallbacks) == len(includes): cmlenz@820: fallback = fallbacks.pop() cmlenz@820: streams.pop() # discard anything between the include tags cmlenz@820: # and the fallback element cmlenz@820: stream = streams[-1] cmlenz@820: href, parse = includes.pop() cmlenz@820: try: cmlenz@820: cls = { cmlenz@820: 'xml': MarkupTemplate, cmlenz@820: 'text': NewTextTemplate cmlenz@820: }[parse or 'xml'] cmlenz@820: except KeyError: cmlenz@820: raise TemplateSyntaxError('Invalid value for "parse" ' cmlenz@820: 'attribute of include', cmlenz@820: self.filepath, *pos[1:]) cmlenz@820: stream.append((INCLUDE, (href, cls, fallback), pos)) cmlenz@820: else: cmlenz@820: stream.append((kind, data, pos)) cmlenz@820: cmlenz@820: elif kind is START_NS and data[1] == xinclude_ns: cmlenz@820: # Strip out the XInclude namespace cmlenz@820: prefixes[data[0]] = data[1] cmlenz@820: cmlenz@820: elif kind is END_NS and data in prefixes: cmlenz@820: prefixes.pop(data) cmlenz@820: cmlenz@820: else: cmlenz@820: stream.append((kind, data, pos)) cmlenz@820: cmlenz@395: assert len(streams) == 1 cmlenz@395: return streams[0] cmlenz@395: cmlenz@820: def _interpolate_attrs(self, stream): cmlenz@820: for kind, data, pos in stream: cmlenz@820: cmlenz@820: if kind is START: cmlenz@820: # Record any directive attributes in start tags cmlenz@820: tag, attrs = data cmlenz@820: new_attrs = [] cmlenz@820: for name, value in attrs: cmlenz@820: if value: cmlenz@820: value = list(interpolate(value, self.filepath, pos[1], cmlenz@820: pos[2], lookup=self.lookup)) cmlenz@820: if len(value) == 1 and value[0][0] is TEXT: cmlenz@820: value = value[0][1] cmlenz@820: new_attrs.append((name, value)) cmlenz@820: data = tag, Attrs(new_attrs) cmlenz@820: cmlenz@820: yield kind, data, pos cmlenz@820: cmlenz@820: def _prepare(self, stream): cmlenz@820: return Template._prepare(self, cmlenz@820: self._extract_includes(self._interpolate_attrs(stream)) cmlenz@820: ) cmlenz@820: cmlenz@820: def add_directives(self, namespace, factory): cmlenz@820: """Register a custom `DirectiveFactory` for a given namespace. cmlenz@820: cmlenz@820: :param namespace: the namespace URI cmlenz@820: :type namespace: `basestring` cmlenz@820: :param factory: the directive factory to register cmlenz@820: :type factory: `DirectiveFactory` cmlenz@820: :since: version 0.6 cmlenz@395: """ cmlenz@820: assert not self._prepared, 'Too late for adding directives, ' \ cmlenz@820: 'template already prepared' cmlenz@820: self._stream = self._extract_directives(self._stream, namespace, cmlenz@820: factory) cmlenz@336: cmlenz@820: def _match(self, stream, ctxt, start=0, end=None, **vars): cmlenz@336: """Internal stream filter that applies any defined match templates cmlenz@336: to the stream. cmlenz@336: """ cmlenz@820: 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@820: for idx, (test, path, template, hints, namespaces, directives) \ cmlenz@820: in enumerate(match_templates): cmlenz@820: if idx < start or end is not None and idx >= end: cmlenz@820: continue cmlenz@336: cmlenz@336: if test(event, namespaces, ctxt) is True: cmlenz@820: if 'match_once' in hints: cmlenz@820: del match_templates[idx] cmlenz@820: 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@820: pre_end = idx + 1 cmlenz@820: if 'match_once' not in hints and 'not_recursive' in hints: cmlenz@820: pre_end -= 1 cmlenz@820: inner = _strip(stream) cmlenz@820: if pre_end > 0: cmlenz@830: inner = self._match(inner, ctxt, end=pre_end, **vars) cmlenz@820: content = self._include(chain([event], inner, tail), ctxt) cmlenz@820: if 'not_buffered' not in hints: cmlenz@820: content = list(content) cmlenz@336: cmlenz@336: # Make the select() function available in the body of the cmlenz@336: # match template cmlenz@820: selected = [False] cmlenz@336: def select(path): cmlenz@820: selected[0] = True cmlenz@336: return Stream(content).select(path, namespaces, ctxt) cmlenz@820: vars = dict(select=select) cmlenz@336: cmlenz@336: # Recursively process the output cmlenz@820: template = _apply_directives(template, directives, ctxt, cmlenz@830: vars) cmlenz@820: for event in self._match(self._flatten(template, ctxt, cmlenz@820: **vars), cmlenz@820: ctxt, start=idx + 1, **vars): cmlenz@336: yield event cmlenz@336: cmlenz@820: # If the match template did not actually call select to cmlenz@820: # consume the matched stream, the original events need to cmlenz@820: # be consumed here or they'll get appended to the output cmlenz@820: if not selected[0]: cmlenz@820: for event in content: cmlenz@820: pass cmlenz@820: cmlenz@820: # Let the remaining match templates know about the last cmlenz@820: # event in the matched content, so they can update their cmlenz@820: # internal state accordingly cmlenz@820: for test in [mt[0] for mt in match_templates]: cmlenz@820: test(tail[0], namespaces, ctxt, updateonly=True) cmlenz@820: cmlenz@336: break cmlenz@336: cmlenz@336: else: # no matches cmlenz@336: yield event