cmlenz@336: # -*- coding: utf-8 -*-
cmlenz@336: #
cmlenz@897: # Copyright (C) 2006-2010 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: ... - ${item}
cmlenz@336: ...
''')
cmlenz@853: >>> print(tmpl.generate(items=[1, 2, 3]))
cmlenz@336:
cmlenz@336: - 1
- 2
- 3
cmlenz@336:
cmlenz@336: """
cmlenz@427:
cmlenz@790: DIRECTIVE_NAMESPACE = 'http://genshi.edgewall.org/'
cmlenz@790: 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@605: serializer = 'xml'
cmlenz@636: _number_conv = Markup
cmlenz@336:
cmlenz@790: def __init__(self, source, filepath=None, filename=None, loader=None,
cmlenz@790: encoding=None, lookup='strict', allow_exec=True):
cmlenz@790: Template.__init__(self, source, filepath=filepath, filename=filename,
cmlenz@790: loader=loader, encoding=encoding, lookup=lookup,
cmlenz@790: allow_exec=allow_exec)
cmlenz@790: self.add_directives(self.DIRECTIVE_NAMESPACE, self)
cmlenz@790:
cmlenz@715: def _init_filters(self):
cmlenz@715: Template._init_filters(self)
cmlenz@496: # Make sure the include filter comes after the match filter
cmlenz@876: self.filters.remove(self._include)
cmlenz@876: self.filters += [self._match, self._include]
cmlenz@336:
cmlenz@374: def _parse(self, source, encoding):
cmlenz@374: if not isinstance(source, Stream):
cmlenz@434: source = XMLParser(source, filename=self.filename,
cmlenz@374: encoding=encoding)
cmlenz@790: stream = []
cmlenz@374:
cmlenz@374: for kind, data, pos in source:
cmlenz@336:
cmlenz@790: if kind is TEXT:
cmlenz@790: for kind, data, pos in interpolate(data, self.filepath, pos[1],
cmlenz@790: pos[2], lookup=self.lookup):
cmlenz@336: stream.append((kind, data, pos))
cmlenz@336:
cmlenz@790: elif kind is PI and data[0] == 'python':
cmlenz@790: if not self.allow_exec:
cmlenz@790: raise TemplateSyntaxError('Python code blocks not allowed',
cmlenz@790: self.filepath, *pos[1:])
cmlenz@790: try:
cmlenz@790: suite = Suite(data[1], self.filepath, pos[1],
cmlenz@790: lookup=self.lookup)
cmlenz@790: except SyntaxError, err:
cmlenz@790: raise TemplateSyntaxError(err, self.filepath,
cmlenz@790: pos[1] + (err.lineno or 1) - 1,
cmlenz@790: pos[2] + (err.offset or 0))
cmlenz@790: stream.append((EXEC, suite, pos))
cmlenz@790:
cmlenz@790: elif kind is COMMENT:
cmlenz@790: if not data.lstrip().startswith('!'):
cmlenz@336: stream.append((kind, data, pos))
cmlenz@336:
cmlenz@790: else:
cmlenz@790: stream.append((kind, data, pos))
cmlenz@790:
cmlenz@790: return stream
cmlenz@790:
cmlenz@790: def _extract_directives(self, stream, namespace, factory):
cmlenz@790: depth = 0
cmlenz@790: dirmap = {} # temporary mapping of directives to elements
cmlenz@790: new_stream = []
cmlenz@790: ns_prefix = {} # namespace prefixes in use
cmlenz@790:
cmlenz@790: for kind, data, pos in stream:
cmlenz@790:
cmlenz@790: if kind is START:
cmlenz@363: tag, attrs = data
cmlenz@336: directives = []
cmlenz@336: strip = False
cmlenz@336:
cmlenz@790: if tag.namespace == namespace:
cmlenz@790: cls = factory.get_directive(tag.localname)
cmlenz@336: if cls is None:
cmlenz@790: raise BadDirectiveError(tag.localname,
cmlenz@790: self.filepath, pos[1])
cmlenz@552: args = dict([(name.localname, value) for name, value
cmlenz@552: in attrs if not name.namespace])
cmlenz@847: directives.append((factory.get_directive_index(cls), cls,
cmlenz@847: args, ns_prefix.copy(), pos))
cmlenz@336: strip = True
cmlenz@336:
cmlenz@363: new_attrs = []
cmlenz@363: for name, value in attrs:
cmlenz@790: if name.namespace == namespace:
cmlenz@790: cls = factory.get_directive(name.localname)
cmlenz@336: if cls is None:
cmlenz@336: raise BadDirectiveError(name.localname,
cmlenz@336: self.filepath, pos[1])
cmlenz@790: if type(value) is list and len(value) == 1:
cmlenz@790: value = value[0][1]
cmlenz@847: directives.append((factory.get_directive_index(cls),
cmlenz@847: cls, value, ns_prefix.copy(), pos))
cmlenz@336: else:
cmlenz@363: new_attrs.append((name, value))
cmlenz@363: new_attrs = Attrs(new_attrs)
cmlenz@336:
cmlenz@336: if directives:
cmlenz@847: directives.sort()
cmlenz@790: dirmap[(depth, tag)] = (directives, len(new_stream),
cmlenz@790: strip)
cmlenz@336:
cmlenz@790: new_stream.append((kind, (tag, new_attrs), pos))
cmlenz@790: depth += 1
cmlenz@790:
cmlenz@790: elif kind is END:
cmlenz@790: depth -= 1
cmlenz@790: new_stream.append((kind, data, pos))
cmlenz@790:
cmlenz@790: # If there have have directive attributes with the
cmlenz@790: # corresponding start tag, move the events inbetween into
cmlenz@790: # a "subprogram"
cmlenz@790: if (depth, data) in dirmap:
cmlenz@790: directives, offset, strip = dirmap.pop((depth, data))
cmlenz@790: substream = new_stream[offset:]
cmlenz@790: if strip:
cmlenz@790: substream = substream[1:-1]
cmlenz@790: new_stream[offset:] = [
cmlenz@790: (SUB, (directives, substream), pos)
cmlenz@790: ]
cmlenz@790:
cmlenz@790: elif kind is SUB:
cmlenz@790: directives, substream = data
cmlenz@790: substream = self._extract_directives(substream, namespace,
cmlenz@790: factory)
cmlenz@790:
cmlenz@790: if len(substream) == 1 and substream[0][0] is SUB:
cmlenz@790: added_directives, substream = substream[0][1]
cmlenz@790: directives += added_directives
cmlenz@790:
cmlenz@790: new_stream.append((kind, (directives, substream), pos))
cmlenz@790:
cmlenz@790: elif kind is START_NS:
cmlenz@790: # Strip out the namespace declaration for template
cmlenz@790: # directives
cmlenz@790: prefix, uri = data
cmlenz@790: ns_prefix[prefix] = uri
cmlenz@790: if uri != namespace:
cmlenz@790: new_stream.append((kind, data, pos))
cmlenz@790:
cmlenz@790: elif kind is END_NS:
cmlenz@790: uri = ns_prefix.pop(data, None)
cmlenz@790: if uri and uri != namespace:
cmlenz@790: new_stream.append((kind, data, pos))
cmlenz@790:
cmlenz@790: else:
cmlenz@790: new_stream.append((kind, data, pos))
cmlenz@790:
cmlenz@790: return new_stream
cmlenz@790:
cmlenz@790: def _extract_includes(self, stream):
cmlenz@790: streams = [[]] # stacked lists of events of the "compiled" template
cmlenz@790: prefixes = {}
cmlenz@790: fallbacks = []
cmlenz@790: includes = []
cmlenz@790: xinclude_ns = Namespace(self.XINCLUDE_NAMESPACE)
cmlenz@790:
cmlenz@790: for kind, data, pos in stream:
cmlenz@790: stream = streams[-1]
cmlenz@790:
cmlenz@790: if kind is START:
cmlenz@790: # Record any directive attributes in start tags
cmlenz@790: tag, attrs = data
cmlenz@790: if tag in xinclude_ns:
cmlenz@363: if tag.localname == 'include':
cmlenz@790: include_href = 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@790: includes.append((include_href, 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: else:
cmlenz@790: stream.append((kind, (tag, attrs), pos))
cmlenz@336:
cmlenz@336: elif kind is END:
cmlenz@790: if fallbacks and data == xinclude_ns['fallback']:
cmlenz@590: assert streams.pop() is fallbacks[-1]
cmlenz@790: elif data == xinclude_ns['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@880: }.get(parse) or self.__class__
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@790: elif kind is START_NS and data[1] == xinclude_ns:
cmlenz@790: # Strip out the XInclude namespace
cmlenz@790: prefixes[data[0]] = data[1]
cmlenz@336:
cmlenz@790: elif kind is END_NS and data in prefixes:
cmlenz@790: prefixes.pop(data)
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@790: def _interpolate_attrs(self, stream):
cmlenz@790: for kind, data, pos in stream:
cmlenz@790:
cmlenz@790: if kind is START:
cmlenz@790: # Record any directive attributes in start tags
cmlenz@790: tag, attrs = data
cmlenz@790: new_attrs = []
cmlenz@790: for name, value in attrs:
cmlenz@790: if value:
cmlenz@790: value = list(interpolate(value, self.filepath, pos[1],
cmlenz@790: pos[2], lookup=self.lookup))
cmlenz@790: if len(value) == 1 and value[0][0] is TEXT:
cmlenz@790: value = value[0][1]
cmlenz@790: new_attrs.append((name, value))
cmlenz@790: data = tag, Attrs(new_attrs)
cmlenz@790:
cmlenz@790: yield kind, data, pos
cmlenz@790:
cmlenz@790: def _prepare(self, stream):
cmlenz@790: return Template._prepare(self,
cmlenz@790: self._extract_includes(self._interpolate_attrs(stream))
cmlenz@790: )
cmlenz@790:
cmlenz@790: def add_directives(self, namespace, factory):
cmlenz@790: """Register a custom `DirectiveFactory` for a given namespace.
cmlenz@790:
cmlenz@790: :param namespace: the namespace URI
cmlenz@790: :type namespace: `basestring`
cmlenz@790: :param factory: the directive factory to register
cmlenz@790: :type factory: `DirectiveFactory`
cmlenz@790: :since: version 0.6
cmlenz@790: """
cmlenz@790: assert not self._prepared, 'Too late for adding directives, ' \
cmlenz@790: 'template already prepared'
cmlenz@790: self._stream = self._extract_directives(self._stream, namespace,
cmlenz@790: factory)
cmlenz@790:
cmlenz@766: 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@766: match_templates = ctxt._match_templates
cmlenz@336:
hodgestar@921: def _strip(stream, append):
cmlenz@336: depth = 1
cmlenz@843: next = stream.next
cmlenz@336: while 1:
cmlenz@843: event = 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@843: append(event)
cmlenz@336: break
cmlenz@336:
cmlenz@336: for event in stream:
cmlenz@336:
cmlenz@809: # We (currently) only care about start and end events for matching
cmlenz@336: # We might care about namespace events in the future, though
cmlenz@809: if not match_templates or (event[0] is not START and
cmlenz@809: 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@766: if idx < start or end is not None and idx >= end:
cmlenz@758: continue
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@766: pre_end = idx + 1
cmlenz@694: if 'match_once' not in hints and 'not_recursive' in hints:
cmlenz@766: pre_end -= 1
hodgestar@921: tail = []
hodgestar@921: inner = _strip(stream, tail.append)
cmlenz@766: if pre_end > 0:
cmlenz@870: inner = self._match(inner, ctxt, start=start,
cmlenz@870: end=pre_end, **vars)
cmlenz@700: content = self._include(chain([event], inner, tail), ctxt)
cmlenz@700: if 'not_buffered' not in hints:
cmlenz@700: content = list(content)
cmlenz@843: content = Stream(content)
cmlenz@336:
cmlenz@336: # Make the select() function available in the body of the
cmlenz@336: # match template
cmlenz@771: selected = [False]
cmlenz@336: def select(path):
cmlenz@771: selected[0] = True
cmlenz@843: return content.select(path, namespaces, ctxt)
cmlenz@700: vars = dict(select=select)
cmlenz@336:
cmlenz@336: # Recursively process the output
cmlenz@700: template = _apply_directives(template, directives, ctxt,
cmlenz@827: vars)
cmlenz@813: for event in self._match(self._flatten(template, ctxt,
cmlenz@813: **vars),
cmlenz@813: ctxt, start=idx + 1, **vars):
cmlenz@336: yield event
cmlenz@336:
cmlenz@771: # If the match template did not actually call select to
cmlenz@771: # consume the matched stream, the original events need to
cmlenz@771: # be consumed here or they'll get appended to the output
cmlenz@771: if not selected[0]:
cmlenz@771: for event in content:
cmlenz@771: pass
cmlenz@771:
cmlenz@771: # Let the remaining match templates know about the last
cmlenz@771: # event in the matched content, so they can update their
cmlenz@771: # internal state accordingly
cmlenz@874: for test in [mt[0] for mt in match_templates[idx + 1:]]:
cmlenz@771: test(tail[0], namespaces, ctxt, updateonly=True)
cmlenz@771:
cmlenz@336: break
cmlenz@336:
cmlenz@336: else: # no matches
cmlenz@336: yield event