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 Title

Document 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 <body> element and paste the copied text cmlenz@501: into the body as <h1> 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: <html><head><title>Some Title

Some 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 Title

Some 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})