cmlenz@501: # -*- coding: utf-8 -*-
cmlenz@501: #
cmlenz@501: # Copyright (C) 2006-2007 Edgewall Software
cmlenz@501: # All rights reserved.
cmlenz@501: #
cmlenz@501: # This software is licensed as described in the file COPYING, which
cmlenz@501: # you should have received as part of this distribution. The terms
cmlenz@501: # are also available at http://genshi.edgewall.org/wiki/License.
cmlenz@501: #
cmlenz@501: # This software consists of voluntary contributions made by many
cmlenz@501: # individuals. For the exact contribution history, see the revision
cmlenz@501: # history and logs, available at http://genshi.edgewall.org/log/.
cmlenz@501:
cmlenz@501: """A filter for generalised functional-style transformations of markup streams,
cmlenz@501: inspired by JQuery.
cmlenz@501: """
cmlenz@501:
cmlenz@501: import sys
cmlenz@501:
cmlenz@501: from genshi.path import Path
cmlenz@501: from genshi.builder import Element
cmlenz@501: from genshi.core import Stream, Attrs, QName, TEXT, START, END
cmlenz@501:
cmlenz@501: __all__ = ['Transformer', 'Injector', 'BEGIN', 'FINISH', 'INSIDE', 'OUTSIDE']
cmlenz@501:
cmlenz@501:
cmlenz@501: class TransformMark(str):
cmlenz@501: """A mark on a transformation stream."""
cmlenz@501: __slots__ = []
cmlenz@501: _instances = {}
cmlenz@501:
cmlenz@501: def __new__(cls, val):
cmlenz@501: return cls._instances.setdefault(val, str.__new__(cls, val))
cmlenz@501:
cmlenz@501:
cmlenz@501: BEGIN = TransformMark('BEGIN')
cmlenz@501: INSIDE = TransformMark('INSIDE')
cmlenz@501: OUTSIDE = TransformMark('OUTSIDE')
cmlenz@501: FINISH = TransformMark('FINISH')
cmlenz@501:
cmlenz@501:
cmlenz@501: class Transformer(object):
cmlenz@501: """Stream filter that can apply a variety of different transformations to
cmlenz@501: a stream.
cmlenz@501:
cmlenz@501: This is achieved by selecting the events to be transformed using XPath,
cmlenz@501: then applying the transformations to the events matched by the path
cmlenz@501: expression. Each marked event is in the form (mark, (kind, data, pos)),
cmlenz@501: where mark can be any of `BEGIN`, `FINISH`, `INSIDE`, `OUTSIDE` or None.
cmlenz@501:
cmlenz@501: The first three marks match `START` and `END` events, and any events
cmlenz@501: contained `INSIDE` any selected XML/HTML element. A non-element match
cmlenz@501: outside a `START`/`END` container (e.g. ``text()``) will yield an `OUTSIDE`
cmlenz@501: mark.
cmlenz@501:
cmlenz@501: >>> stream = HTML('
Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> short_stream = HTML('Some test text')
cmlenz@501:
cmlenz@501: Transformations act on selected stream events matching an XPath. Here's an
cmlenz@501: example of removing some markup (title) selected by an expression:
cmlenz@501:
cmlenz@501: >>> print stream | Transformer('.//title').remove()
cmlenz@501: Some body text.
cmlenz@501:
cmlenz@501: Inserted content can be passed in the form of a string, or a Genshi event
cmlenz@501: Stream, which includes ``genshi.builder.tag``:
cmlenz@501:
cmlenz@501: >>> from genshi.builder import tag
cmlenz@501: >>> print stream | Transformer('.//body').prepend(tag.h1('Document Title'))
cmlenz@501: Some TitleDocument
cmlenz@501: Title
Some body text.
cmlenz@501:
cmlenz@501: Each XPath expression determines the set of tags that will be acted upon by
cmlenz@501: subsequent transformations. In this example we select the text, copy
cmlenz@501: it into a buffer, then select the element and paste the copied text
cmlenz@501: into the body as enclosed text:
cmlenz@501:
cmlenz@501: >>> buffer = []
cmlenz@501: >>> print stream | Transformer('.//title/text()').copy(buffer) \
cmlenz@501: .select('.//body').prepend(tag.h1(buffer))
cmlenz@501: Some TitleSome Title
Some
cmlenz@501: body text.
cmlenz@501:
cmlenz@501: Transformations can also be assigned and reused, although care must be
cmlenz@501: taken when using buffers, to ensure that buffers are cleared between
cmlenz@501: transforms:
cmlenz@501:
cmlenz@501: >>> emphasis = Transformer('.//em').setattr('class', 'emphasis')
cmlenz@501: >>> print stream | emphasis
cmlenz@501: Some TitleSome body text.
cmlenz@501: >>> print HTML('Emphasis') | emphasis
cmlenz@501: Emphasis
cmlenz@501: """
cmlenz@501:
cmlenz@501: __slots__ = ('transforms',)
cmlenz@501:
cmlenz@501: def __init__(self, path=None):
cmlenz@501: """Construct a new transformation filter.
cmlenz@501:
cmlenz@501: :param path: the XPath expression
cmlenz@501: """
cmlenz@501: self.transforms = []
cmlenz@501: if path:
cmlenz@501: self.transforms.append(Select(path))
cmlenz@501:
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: :return: the transformed stream
cmlenz@501: :rtype: `Stream`
cmlenz@501: """
cmlenz@501: transforms = self._mark(stream)
cmlenz@501: for link in self.transforms:
cmlenz@501: transforms = link(transforms)
cmlenz@501: return Stream(self._unmark(transforms))
cmlenz@501:
cmlenz@501: def __or__(self, function):
cmlenz@501: """Combine transformations.
cmlenz@501:
cmlenz@501: Transformations can be chained, similar to stream filters. Any callable
cmlenz@501: accepting a marked stream can be used as a transform.
cmlenz@501:
cmlenz@501: As an example, here is a simple `TEXT` event upper-casing transform:
cmlenz@501:
cmlenz@501: >>> def upper(stream):
cmlenz@501: ... for mark, (kind, data, pos) in stream:
cmlenz@501: ... if mark and kind is TEXT:
cmlenz@501: ... yield mark, (kind, data.upper(), pos)
cmlenz@501: ... else:
cmlenz@501: ... yield mark, (kind, data, pos)
cmlenz@501: >>> short_stream = HTML('Some test text')
cmlenz@501: >>> print short_stream | (Transformer('.//em/text()') | upper)
cmlenz@501: Some TEST text
cmlenz@501: """
cmlenz@501: transform = Transformer()
cmlenz@501: transform.transforms = self.transforms[:]
cmlenz@501: if isinstance(function, Transformer):
cmlenz@501: transform.transforms.extend(function.transforms)
cmlenz@501: else:
cmlenz@501: transform.transforms.append(function)
cmlenz@501: return transform
cmlenz@501:
cmlenz@501: #{ Selection operations
cmlenz@501:
cmlenz@501: def select(self, path):
cmlenz@501: """Mark events matching the given XPath expression.
cmlenz@501:
cmlenz@501: >>> html = HTML('Some test text')
cmlenz@501: >>> print html | Transformer().select('.//em').trace()
cmlenz@501: (None, ('START', (QName(u'body'), Attrs()), (None, 1, 0)))
cmlenz@501: (None, ('TEXT', u'Some ', (None, 1, 6)))
cmlenz@501: ('BEGIN', ('START', (QName(u'em'), Attrs()), (None, 1, 11)))
cmlenz@501: ('INSIDE', ('TEXT', u'test', (None, 1, 15)))
cmlenz@501: ('FINISH', ('END', QName(u'em'), (None, 1, 19)))
cmlenz@501: (None, ('TEXT', u' text', (None, 1, 24)))
cmlenz@501: (None, ('END', QName(u'body'), (None, 1, 29)))
cmlenz@501: Some test text
cmlenz@501:
cmlenz@501: :return: the stream augmented by transformation marks
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | Select(path)
cmlenz@501:
cmlenz@501: def invert(self):
cmlenz@501: """Invert selection so that marked events become unmarked, and vice
cmlenz@501: versa.
cmlenz@501:
cmlenz@501: Specificaly, all marks are converted to null marks, and all null marks
cmlenz@501: are converted to OUTSIDE marks.
cmlenz@501:
cmlenz@501: >>> html = HTML('Some test text')
cmlenz@501: >>> print html | Transformer('//em').invert().trace()
cmlenz@501: ('OUTSIDE', ('START', (QName(u'body'), Attrs()), (None, 1, 0)))
cmlenz@501: ('OUTSIDE', ('TEXT', u'Some ', (None, 1, 6)))
cmlenz@501: (None, ('START', (QName(u'em'), Attrs()), (None, 1, 11)))
cmlenz@501: (None, ('TEXT', u'test', (None, 1, 15)))
cmlenz@501: (None, ('END', QName(u'em'), (None, 1, 19)))
cmlenz@501: ('OUTSIDE', ('TEXT', u' text', (None, 1, 24)))
cmlenz@501: ('OUTSIDE', ('END', QName(u'body'), (None, 1, 29)))
cmlenz@501: Some test text
cmlenz@501:
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | invert
cmlenz@501:
cmlenz@501: #{ Deletion operations
cmlenz@501:
cmlenz@501: def empty(self):
cmlenz@501: """Empty selected elements of all content.
cmlenz@501:
cmlenz@501: Example:
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//em').empty()
cmlenz@501: Some TitleSome
cmlenz@501: text.
cmlenz@501:
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | empty
cmlenz@501:
cmlenz@501: def remove(self):
cmlenz@501: """Remove selection from the stream.
cmlenz@501:
cmlenz@501: Example:
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//em').remove()
cmlenz@501: Some TitleSome
cmlenz@501: text.
cmlenz@501:
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | remove
cmlenz@501:
cmlenz@501: #{ Direct element operations
cmlenz@501:
cmlenz@501: def unwrap(self):
cmlenz@501: """Remove outtermost enclosing elements from selection.
cmlenz@501:
cmlenz@501: Example:
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//em').unwrap()
cmlenz@501: Some TitleSome body
cmlenz@501: text.
cmlenz@501:
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | unwrap
cmlenz@501:
cmlenz@501:
cmlenz@501: def wrap(self, element):
cmlenz@501: """Wrap selection in an element.
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//em').wrap('strong')
cmlenz@501: Some TitleSome
cmlenz@501: body text.
cmlenz@501:
cmlenz@501: :param element: Either a string tag name or a Genshi builder element.
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | Wrap(element)
cmlenz@501:
cmlenz@501: #{ Content insertion operations
cmlenz@501:
cmlenz@501: def replace(self, content):
cmlenz@501: """Replace selection with content.
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//title/text()').replace('New Title')
cmlenz@501: New TitleSome body
cmlenz@501: text.
cmlenz@501:
cmlenz@501: :param content: Either an iterable of events or a string to insert.
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | Replace(content)
cmlenz@501:
cmlenz@501: def before(self, content):
cmlenz@501: """Insert content before selection.
cmlenz@501:
cmlenz@501: In this example we insert the word 'emphasised' before the opening
cmlenz@501: tag:
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//em').before('emphasised ')
cmlenz@501: Some TitleSome emphasised
cmlenz@501: body text.
cmlenz@501:
cmlenz@501: :param content: Either an iterable of events or a string to insert.
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | Before(content)
cmlenz@501:
cmlenz@501: def after(self, content):
cmlenz@501: """Insert content after selection.
cmlenz@501:
cmlenz@501: Here, we insert some text after the closing tag:
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//em').after(' rock')
cmlenz@501: Some TitleSome body
cmlenz@501: rock text.
cmlenz@501:
cmlenz@501: :param content: Either an iterable of events or a string to insert.
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | After(content)
cmlenz@501:
cmlenz@501: def prepend(self, content):
cmlenz@501: """Insert content after the BEGIN event of the selection.
cmlenz@501:
cmlenz@501: Inserting some new text at the start of the :
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//body').prepend('Some new body text. ')
cmlenz@501: Some TitleSome new body text.
cmlenz@501: Some body text.
cmlenz@501:
cmlenz@501: :param content: Either an iterable of events or a string to insert.
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | Prepend(content)
cmlenz@501:
cmlenz@501: def append(self, content):
cmlenz@501: """Insert content before the END event of the selection.
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//body').append(' Some new body text.')
cmlenz@501: Some TitleSome body
cmlenz@501: text. Some new body text.
cmlenz@501:
cmlenz@501: :param content: Either an iterable of events or a string to insert.
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | Append(content)
cmlenz@501:
cmlenz@501: #{ Attribute manipulation
cmlenz@501:
cmlenz@501: def setattr(self, key, value):
cmlenz@501: """Add or replace an attribute to selected elements.
cmlenz@501:
cmlenz@501: Example:
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//em').setattr('class', 'emphasis')
cmlenz@501: Some TitleSome body text.
cmlenz@501:
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | SetAttr(key, value)
cmlenz@501:
cmlenz@501: def delattr(self, key):
cmlenz@501: """Delete an attribute from selected elements.
cmlenz@501:
cmlenz@501: Example:
cmlenz@501:
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body '
cmlenz@501: ... 'text.')
cmlenz@501: >>> print html | Transformer('.//*[@class="emphasis"]').delattr('class')
cmlenz@501: Some TitleSome body
cmlenz@501: text.
cmlenz@501:
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | DelAttr(key)
cmlenz@501:
cmlenz@501: #{ Buffer operations
cmlenz@501:
cmlenz@501: def copy(self, buffer):
cmlenz@501: """Copy selection into buffer.
cmlenz@501:
cmlenz@501: >>> from genshi.builder import tag
cmlenz@501: >>> buffer = []
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//title/text()').copy(buffer) \\
cmlenz@501: ... .select('.//body').prepend(tag.h1(buffer))
cmlenz@501: Some TitleSome Title
Some
cmlenz@501: body text.
cmlenz@501:
cmlenz@501: :param buffer: a list-like object (must support .append() and be
cmlenz@501: iterable) where the selection will be buffered.
cmlenz@501: :rtype: `Transformer`
cmlenz@501: :note: this transformation will buffer the entire input stream
cmlenz@501: """
cmlenz@501: return self | Copy(buffer)
cmlenz@501:
cmlenz@501: def cut(self, buffer):
cmlenz@501: """Copy selection into buffer and remove the selection from the stream.
cmlenz@501:
cmlenz@501: >>> from genshi.builder import tag
cmlenz@501: >>> buffer = []
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('.//em/text()').cut(buffer) \\
cmlenz@501: ... .select('.//em').after(tag.h1(buffer))
cmlenz@501: Some TitleSome
cmlenz@501: body
text.
cmlenz@501:
cmlenz@501: :param buffer: a list-like object (must support .append() and be
cmlenz@501: iterable) where the selection will be buffered.
cmlenz@501: :rtype: `Transformer`
cmlenz@501: :note: this transformation will buffer the entire input stream
cmlenz@501: """
cmlenz@501: return self | Cut(buffer)
cmlenz@501:
cmlenz@501: #{ Miscellaneous operations
cmlenz@501:
cmlenz@501: def apply(self, function, kind):
cmlenz@501: """Apply a function to the ``data`` element of events of ``kind`` in
cmlenz@501: the selection.
cmlenz@501:
cmlenz@501: >>> import string
cmlenz@501: >>> html = HTML('Some Title'
cmlenz@501: ... 'Some body text.')
cmlenz@501: >>> print html | Transformer('head/title').apply(string.upper, TEXT)
cmlenz@501: SOME TITLESome body
cmlenz@501: text.
cmlenz@501:
cmlenz@501: :param function: the function to apply
cmlenz@501: :param kind: the kind of event the function should be applied to
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | Apply(function, kind)
cmlenz@501:
cmlenz@501: def trace(self, prefix='', fileobj=None):
cmlenz@501: """Print events as they pass through the transform.
cmlenz@501:
cmlenz@501: >>> html = HTML('Some test text')
cmlenz@501: >>> print html | Transformer('em').trace()
cmlenz@501: (None, ('START', (QName(u'body'), Attrs()), (None, 1, 0)))
cmlenz@501: (None, ('TEXT', u'Some ', (None, 1, 6)))
cmlenz@501: ('BEGIN', ('START', (QName(u'em'), Attrs()), (None, 1, 11)))
cmlenz@501: ('INSIDE', ('TEXT', u'test', (None, 1, 15)))
cmlenz@501: ('FINISH', ('END', QName(u'em'), (None, 1, 19)))
cmlenz@501: (None, ('TEXT', u' text', (None, 1, 24)))
cmlenz@501: (None, ('END', QName(u'body'), (None, 1, 29)))
cmlenz@501: Some test text
cmlenz@501:
cmlenz@501: :param prefix: a string to prefix each event with in the output
cmlenz@501: :param fileobj: the writable file-like object to write to; defaults to
cmlenz@501: the standard output stream
cmlenz@501: :rtype: `Transformer`
cmlenz@501: """
cmlenz@501: return self | Trace(prefix, fileobj=fileobj)
cmlenz@501:
cmlenz@501: # Internal methods
cmlenz@501:
cmlenz@501: def _mark(self, stream):
cmlenz@501: for event in stream:
cmlenz@501: yield None, event
cmlenz@501:
cmlenz@501: def _unmark(self, stream):
cmlenz@501: for mark, event in stream:
cmlenz@501: yield event
cmlenz@501:
cmlenz@501:
cmlenz@501: class Select(object):
cmlenz@501: """Select and mark events that match an XPath expression."""
cmlenz@501: def __init__(self, path):
cmlenz@501: """Create selection.
cmlenz@501:
cmlenz@501: :param path: XPath expression.
cmlenz@501: """
cmlenz@501: self.path = Path(path)
cmlenz@501:
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: namespaces = {}
cmlenz@501: variables = {}
cmlenz@501: test = self.path.test()
cmlenz@501: stream = iter(stream)
cmlenz@501: for mark, event in stream:
cmlenz@501: result = test(event, {}, {})
cmlenz@501: if result is True:
cmlenz@501: if event[0] is START:
cmlenz@501: yield BEGIN, event
cmlenz@501: depth = 1
cmlenz@501: while depth > 0:
cmlenz@501: mark, subevent = stream.next()
cmlenz@501: if subevent[0] is START:
cmlenz@501: depth += 1
cmlenz@501: elif subevent[0] is END:
cmlenz@501: depth -= 1
cmlenz@501: if depth == 0:
cmlenz@501: yield FINISH, subevent
cmlenz@501: else:
cmlenz@501: yield INSIDE, subevent
cmlenz@501: test(subevent, {}, {}, updateonly=True)
cmlenz@501: else:
cmlenz@501: yield OUTSIDE, event
cmlenz@501: elif result:
cmlenz@501: yield BEGIN, result
cmlenz@501: else:
cmlenz@501: yield None, event
cmlenz@501:
cmlenz@501:
cmlenz@501: def invert(stream):
cmlenz@501: """Invert selection so that marked events become unmarked, and vice versa.
cmlenz@501:
cmlenz@501: Specificaly, all input marks are converted to null marks, and all input
cmlenz@501: null marks are converted to OUTSIDE marks.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: if mark:
cmlenz@501: yield None, event
cmlenz@501: else:
cmlenz@501: yield OUTSIDE, event
cmlenz@501:
cmlenz@501: def empty(stream):
cmlenz@501: """Empty selected elements of all content.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: if mark not in (INSIDE, OUTSIDE):
cmlenz@501: yield mark, event
cmlenz@501:
cmlenz@501: def remove(stream):
cmlenz@501: """Remove selection from the stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: if mark is None:
cmlenz@501: yield mark, event
cmlenz@501:
cmlenz@501: def unwrap(stream):
cmlenz@501: """Remove outtermost enclosing elements from selection.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: if mark not in (BEGIN, FINISH):
cmlenz@501: yield mark, event
cmlenz@501:
cmlenz@501:
cmlenz@501: class Wrap(object):
cmlenz@501: """Wrap selection in an element."""
cmlenz@501:
cmlenz@501: def __init__(self, element):
cmlenz@501: if isinstance(element, Element):
cmlenz@501: self.element = element
cmlenz@501: else:
cmlenz@501: self.element = Element(element)
cmlenz@501:
cmlenz@501: def __call__(self, stream):
cmlenz@501: for mark, event in stream:
cmlenz@501: if mark:
cmlenz@501: element = list(self.element.generate())
cmlenz@501: for prefix in element[:-1]:
cmlenz@501: yield None, prefix
cmlenz@501: yield mark, event
cmlenz@501: while True:
cmlenz@501: mark, event = stream.next()
cmlenz@501: if not mark:
cmlenz@501: break
cmlenz@501: yield mark, event
cmlenz@501: yield None, element[-1]
cmlenz@501: yield mark, event
cmlenz@501: else:
cmlenz@501: yield mark, event
cmlenz@501:
cmlenz@501:
cmlenz@501: class Trace(object):
cmlenz@501: """Print events as they pass through the transform."""
cmlenz@501:
cmlenz@501: def __init__(self, prefix='', fileobj=None):
cmlenz@501: """Trace constructor.
cmlenz@501:
cmlenz@501: :param prefix: text to prefix each traced line with.
cmlenz@501: :param fileobj: the writable file-like object to write to
cmlenz@501: """
cmlenz@501: self.prefix = prefix
cmlenz@501: self.fileobj = fileobj or sys.stdout
cmlenz@501:
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for event in stream:
cmlenz@501: print>>self.fileobj, self.prefix + str(event)
cmlenz@501: yield event
cmlenz@501:
cmlenz@501:
cmlenz@501: class Apply(object):
cmlenz@501: """Apply a function to the `data` element of events of ``kind`` in the
cmlenz@501: selection.
cmlenz@501: """
cmlenz@501:
cmlenz@501: def __init__(self, function, kind):
cmlenz@501: """Create the transform.
cmlenz@501:
cmlenz@501: :param function: The function to apply. The function must take one
cmlenz@501: argument, the `data` element of each selected event.
cmlenz@501: :param kind: The Genshi event `kind` to apply ``function`` to.
cmlenz@501: """
cmlenz@501: self.function = function
cmlenz@501: self.kind = kind
cmlenz@501:
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, (kind, data, pos) in stream:
cmlenz@501: if mark and kind == self.kind:
cmlenz@501: yield mark, (kind, self.function(data), pos)
cmlenz@501: else:
cmlenz@501: yield mark, (kind, data, pos)
cmlenz@501:
cmlenz@501:
cmlenz@501: class Injector(object):
cmlenz@501: """Abstract base class for transformations that inject content into a
cmlenz@501: stream.
cmlenz@501:
cmlenz@501: >>> class Top(Injector):
cmlenz@501: ... def __call__(self, stream):
cmlenz@501: ... for event in self._inject():
cmlenz@501: ... yield event
cmlenz@501: ... for event in stream:
cmlenz@501: ... yield event
cmlenz@501: >>> html = HTML('Some test text')
cmlenz@501: >>> print html | (Transformer('.//em') | Top('Prefix '))
cmlenz@501: Prefix Some test text
cmlenz@501: """
cmlenz@501: def __init__(self, content):
cmlenz@501: """Create a new injector.
cmlenz@501:
cmlenz@501: :param content: An iterable of Genshi stream events, or a string to be
cmlenz@501: injected.
cmlenz@501: """
cmlenz@501: self.content = content
cmlenz@501:
cmlenz@501: def _inject(self):
cmlenz@501: if isinstance(self.content, basestring):
cmlenz@501: yield None, (TEXT, self.content, (None, -1, -1))
cmlenz@501: else:
cmlenz@501: for event in self.content:
cmlenz@501: yield None, event
cmlenz@501:
cmlenz@501:
cmlenz@501: class Replace(Injector):
cmlenz@501: """Replace selection with content."""
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: if mark is not None:
cmlenz@501: for subevent in self._inject():
cmlenz@501: yield subevent
cmlenz@501: while True:
cmlenz@501: mark, event = stream.next()
cmlenz@501: if mark is None:
cmlenz@501: yield mark, event
cmlenz@501: break
cmlenz@501: else:
cmlenz@501: yield mark, event
cmlenz@501:
cmlenz@501:
cmlenz@501: class Before(Injector):
cmlenz@501: """Insert content before selection."""
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: if mark in (BEGIN, OUTSIDE):
cmlenz@501: for subevent in self._inject():
cmlenz@501: yield subevent
cmlenz@501: yield mark, event
cmlenz@501:
cmlenz@501:
cmlenz@501: class After(Injector):
cmlenz@501: """Insert content after selection."""
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: yield mark, event
cmlenz@501: if mark:
cmlenz@501: while True:
cmlenz@501: mark, event = stream.next()
cmlenz@501: if not mark:
cmlenz@501: break
cmlenz@501: yield mark, event
cmlenz@501: for subevent in self._inject():
cmlenz@501: yield subevent
cmlenz@501: yield mark, event
cmlenz@501:
cmlenz@501:
cmlenz@501: class Prepend(Injector):
cmlenz@501: """Prepend content to the inside of selected elements."""
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: yield mark, event
cmlenz@501: if mark in (BEGIN, OUTSIDE):
cmlenz@501: for subevent in self._inject():
cmlenz@501: yield subevent
cmlenz@501:
cmlenz@501:
cmlenz@501: class Append(Injector):
cmlenz@501: """Append content after the content of selected elements."""
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, event in stream:
cmlenz@501: yield mark, event
cmlenz@501: if mark is BEGIN:
cmlenz@501: while True:
cmlenz@501: mark, event = stream.next()
cmlenz@501: if mark is FINISH:
cmlenz@501: break
cmlenz@501: yield mark, event
cmlenz@501: for subevent in self._inject():
cmlenz@501: yield subevent
cmlenz@501: yield mark, event
cmlenz@501:
cmlenz@501:
cmlenz@501: class SetAttr(object):
cmlenz@501: """Set an attribute on selected elements."""
cmlenz@501: def __init__(self, key, value):
cmlenz@501: """Construct transform.
cmlenz@501:
cmlenz@501: :param key: Attribute to set.
cmlenz@501: :param value: Value of attribute.
cmlenz@501: """
cmlenz@501: self.key = key
cmlenz@501: self.value = value
cmlenz@501:
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, (kind, data, pos) in stream:
cmlenz@501: if mark is BEGIN:
cmlenz@501: data = (data[0], data[1] | [(QName(self.key), self.value)])
cmlenz@501: yield mark, (kind, data, pos)
cmlenz@501:
cmlenz@501:
cmlenz@501: class DelAttr(object):
cmlenz@501: """Delete an attribute of selected elements."""
cmlenz@501: def __init__(self, key):
cmlenz@501: """Construct transform.
cmlenz@501:
cmlenz@501: :param key: The attribute to remove."""
cmlenz@501: self.key = key
cmlenz@501:
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: for mark, (kind, data, pos) in stream:
cmlenz@501: if mark is BEGIN:
cmlenz@501: data = (data[0], data[1] - self.key)
cmlenz@501: yield mark, (kind, data, pos)
cmlenz@501:
cmlenz@501:
cmlenz@501: class Copy(object):
cmlenz@501: """Copy selected events into a buffer for later insertion."""
cmlenz@501: def __init__(self, buffer):
cmlenz@501: """Create a Copy transform filter.
cmlenz@501:
cmlenz@501: :param buffer: A list-like object (must support .append() and be
cmlenz@501: iterable) where the buffered events will be stored.
cmlenz@501: """
cmlenz@501: self.buffer = buffer
cmlenz@501:
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: stream = list(stream)
cmlenz@501: for mark, event in stream:
cmlenz@501: if mark:
cmlenz@501: self.buffer.append(event)
cmlenz@501: return stream
cmlenz@501:
cmlenz@501:
cmlenz@501: class Cut(Copy):
cmlenz@501: """Cut selected events into a buffer for later insertion and remove the
cmlenz@501: selection."""
cmlenz@501: def __call__(self, stream):
cmlenz@501: """Apply the transform filter to the marked stream.
cmlenz@501:
cmlenz@501: :param stream: The marked event stream to filter
cmlenz@501: """
cmlenz@501: stream = Copy.__call__(self, stream)
cmlenz@501: return remove(stream)
cmlenz@501:
cmlenz@501:
cmlenz@501: if __name__ == '__main__':
cmlenz@501: import doctest
cmlenz@501: from genshi.input import HTML
cmlenz@501: doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE,
cmlenz@501: extraglobs={'HTML': HTML})