Edgewall Software

source: tags/0.4.1/genshi/core.py

Last change on this file was 561, checked in by cmlenz, 16 years ago

Ported [560] to 0.4.x.

  • Property svn:eol-style set to native
File size: 19.6 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2007 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Core classes for markup processing."""
15
16import operator
17
18from genshi.util import plaintext, stripentities, striptags
19
20__all__ = ['Stream', 'Markup', 'escape', 'unescape', 'Attrs', 'Namespace',
21           'QName']
22__docformat__ = 'restructuredtext en'
23
24
25class StreamEventKind(str):
26    """A kind of event on a markup stream."""
27    __slots__ = []
28    _instances = {}
29
30    def __new__(cls, val):
31        return cls._instances.setdefault(val, str.__new__(cls, val))
32
33
34class Stream(object):
35    """Represents a stream of markup events.
36   
37    This class is basically an iterator over the events.
38   
39    Stream events are tuples of the form::
40   
41      (kind, data, position)
42   
43    where ``kind`` is the event kind (such as `START`, `END`, `TEXT`, etc),
44    ``data`` depends on the kind of event, and ``position`` is a
45    ``(filename, line, offset)`` tuple that contains the location of the
46    original element or text in the input. If the original location is unknown,
47    ``position`` is ``(None, -1, -1)``.
48   
49    Also provided are ways to serialize the stream to text. The `serialize()`
50    method will return an iterator over generated strings, while `render()`
51    returns the complete generated text at once. Both accept various parameters
52    that impact the way the stream is serialized.
53    """
54    __slots__ = ['events']
55
56    START = StreamEventKind('START') #: a start tag
57    END = StreamEventKind('END') #: an end tag
58    TEXT = StreamEventKind('TEXT') #: literal text
59    XML_DECL = StreamEventKind('XML_DECL') #: XML declaration
60    DOCTYPE = StreamEventKind('DOCTYPE') #: doctype declaration
61    START_NS = StreamEventKind('START_NS') #: start namespace mapping
62    END_NS = StreamEventKind('END_NS') #: end namespace mapping
63    START_CDATA = StreamEventKind('START_CDATA') #: start CDATA section
64    END_CDATA = StreamEventKind('END_CDATA') #: end CDATA section
65    PI = StreamEventKind('PI') #: processing instruction
66    COMMENT = StreamEventKind('COMMENT') #: comment
67
68    def __init__(self, events):
69        """Initialize the stream with a sequence of markup events.
70       
71        :param events: a sequence or iterable providing the events
72        """
73        self.events = events
74
75    def __iter__(self):
76        return iter(self.events)
77
78    def __or__(self, function):
79        """Override the "bitwise or" operator to apply filters or serializers
80        to the stream, providing a syntax similar to pipes on Unix shells.
81       
82        Assume the following stream produced by the `HTML` function:
83       
84        >>> from genshi.input import HTML
85        >>> html = HTML('''<p onclick="alert('Whoa')">Hello, world!</p>''')
86        >>> print html
87        <p onclick="alert('Whoa')">Hello, world!</p>
88       
89        A filter such as the HTML sanitizer can be applied to that stream using
90        the pipe notation as follows:
91       
92        >>> from genshi.filters import HTMLSanitizer
93        >>> sanitizer = HTMLSanitizer()
94        >>> print html | sanitizer
95        <p>Hello, world!</p>
96       
97        Filters can be any function that accepts and produces a stream (where
98        a stream is anything that iterates over events):
99       
100        >>> def uppercase(stream):
101        ...     for kind, data, pos in stream:
102        ...         if kind is TEXT:
103        ...             data = data.upper()
104        ...         yield kind, data, pos
105        >>> print html | sanitizer | uppercase
106        <p>HELLO, WORLD!</p>
107       
108        Serializers can also be used with this notation:
109       
110        >>> from genshi.output import TextSerializer
111        >>> output = TextSerializer()
112        >>> print html | sanitizer | uppercase | output
113        HELLO, WORLD!
114       
115        Commonly, serializers should be used at the end of the "pipeline";
116        using them somewhere in the middle may produce unexpected results.
117        """
118        return Stream(_ensure(function(self)))
119
120    def filter(self, *filters):
121        """Apply filters to the stream.
122       
123        This method returns a new stream with the given filters applied. The
124        filters must be callables that accept the stream object as parameter,
125        and return the filtered stream.
126       
127        The call::
128       
129            stream.filter(filter1, filter2)
130       
131        is equivalent to::
132       
133            stream | filter1 | filter2
134        """
135        return reduce(operator.or_, (self,) + filters)
136
137    def render(self, method='xml', encoding='utf-8', **kwargs):
138        """Return a string representation of the stream.
139       
140        :param method: determines how the stream is serialized; can be either
141                       "xml", "xhtml", "html", "text", or a custom serializer
142                       class
143        :param encoding: how the output string should be encoded; if set to
144                         `None`, this method returns a `unicode` object
145
146        Any additional keyword arguments are passed to the serializer, and thus
147        depend on the `method` parameter value.
148       
149        :see: XMLSerializer.__init__, XHTMLSerializer.__init__,
150              HTMLSerializer.__init__, TextSerializer.__init__
151        """
152        from genshi.output import encode
153        generator = self.serialize(method=method, **kwargs)
154        return encode(generator, method=method, encoding=encoding)
155
156    def select(self, path, namespaces=None, variables=None):
157        """Return a new stream that contains the events matching the given
158        XPath expression.
159       
160        :param path: a string containing the XPath expression
161        :param namespaces: mapping of namespace prefixes used in the path
162        :param variables: mapping of variable names to values
163        :return: the selected substream
164        :raises PathSyntaxError: if the given path expression is invalid or not
165                                 supported
166        """
167        from genshi.path import Path
168        return Path(path).select(self, namespaces, variables)
169
170    def serialize(self, method='xml', **kwargs):
171        """Generate strings corresponding to a specific serialization of the
172        stream.
173       
174        Unlike the `render()` method, this method is a generator that returns
175        the serialized output incrementally, as opposed to returning a single
176        string.
177       
178        :param method: determines how the stream is serialized; can be either
179                       "xml", "xhtml", "html", "text", or a custom serializer
180                       class
181       
182        Any additional keyword arguments are passed to the serializer, and thus
183        depend on the `method` parameter value.
184       
185        :see: XMLSerializer.__init__, XHTMLSerializer.__init__,
186              HTMLSerializer.__init__, TextSerializer.__init__
187        """
188        from genshi.output import get_serializer
189        return get_serializer(method, **kwargs)(_ensure(self))
190
191    def __str__(self):
192        return self.render()
193
194    def __unicode__(self):
195        return self.render(encoding=None)
196
197
198START = Stream.START
199END = Stream.END
200TEXT = Stream.TEXT
201XML_DECL = Stream.XML_DECL
202DOCTYPE = Stream.DOCTYPE
203START_NS = Stream.START_NS
204END_NS = Stream.END_NS
205START_CDATA = Stream.START_CDATA
206END_CDATA = Stream.END_CDATA
207PI = Stream.PI
208COMMENT = Stream.COMMENT
209
210def _ensure(stream):
211    """Ensure that every item on the stream is actually a markup event."""
212    for event in stream:
213        if type(event) is not tuple:
214            if hasattr(event, 'totuple'):
215                event = event.totuple()
216            else:
217                event = TEXT, unicode(event), (None, -1, -1)
218        yield event
219
220
221class Attrs(tuple):
222    """Immutable sequence type that stores the attributes of an element.
223   
224    Ordering of the attributes is preserved, while access by name is also
225    supported.
226   
227    >>> attrs = Attrs([('href', '#'), ('title', 'Foo')])
228    >>> attrs
229    Attrs([('href', '#'), ('title', 'Foo')])
230   
231    >>> 'href' in attrs
232    True
233    >>> 'tabindex' in attrs
234    False
235    >>> attrs.get('title')
236    'Foo'
237   
238    Instances may not be manipulated directly. Instead, the operators ``|`` and
239    ``-`` can be used to produce new instances that have specific attributes
240    added, replaced or removed.
241   
242    To remove an attribute, use the ``-`` operator. The right hand side can be
243    either a string or a set/sequence of strings, identifying the name(s) of
244    the attribute(s) to remove:
245   
246    >>> attrs - 'title'
247    Attrs([('href', '#')])
248    >>> attrs - ('title', 'href')
249    Attrs()
250   
251    The original instance is not modified, but the operator can of course be
252    used with an assignment:
253
254    >>> attrs
255    Attrs([('href', '#'), ('title', 'Foo')])
256    >>> attrs -= 'title'
257    >>> attrs
258    Attrs([('href', '#')])
259   
260    To add a new attribute, use the ``|`` operator, where the right hand value
261    is a sequence of ``(name, value)`` tuples (which includes `Attrs`
262    instances):
263   
264    >>> attrs | [('title', 'Bar')]
265    Attrs([('href', '#'), ('title', 'Bar')])
266   
267    If the attributes already contain an attribute with a given name, the value
268    of that attribute is replaced:
269   
270    >>> attrs | [('href', 'http://example.org/')]
271    Attrs([('href', 'http://example.org/')])
272    """
273    __slots__ = []
274
275    def __contains__(self, name):
276        """Return whether the list includes an attribute with the specified
277        name.
278        """
279        for attr, _ in self:
280            if attr == name:
281                return True
282
283    def __getslice__(self, i, j):
284        return Attrs(tuple.__getslice__(self, i, j))
285
286    def __or__(self, attrs):
287        """Return a new instance that contains the attributes in `attrs` in
288        addition to any already existing attributes.
289        """
290        repl = dict([(an, av) for an, av in attrs if an in self])
291        return Attrs([(sn, repl.get(sn, sv)) for sn, sv in self] +
292                     [(an, av) for an, av in attrs if an not in self])
293
294    def __repr__(self):
295        if not self:
296            return 'Attrs()'
297        return 'Attrs([%s])' % ', '.join([repr(item) for item in self])
298
299    def __sub__(self, names):
300        """Return a new instance with all attributes with a name in `names` are
301        removed.
302        """
303        if isinstance(names, basestring):
304            names = (names,)
305        return Attrs([(name, val) for name, val in self if name not in names])
306
307    def get(self, name, default=None):
308        """Return the value of the attribute with the specified name, or the
309        value of the `default` parameter if no such attribute is found.
310       
311        :param name: the name of the attribute
312        :param default: the value to return when the attribute does not exist
313        :return: the attribute value, or the `default` value if that attribute
314                 does not exist
315        """
316        for attr, value in self:
317            if attr == name:
318                return value
319        return default
320
321    def totuple(self):
322        """Return the attributes as a markup event.
323       
324        The returned event is a `TEXT` event, the data is the value of all
325        attributes joined together.
326       
327        >>> Attrs([('href', '#'), ('title', 'Foo')]).totuple()
328        ('TEXT', u'#Foo', (None, -1, -1))
329        """
330        return TEXT, u''.join([x[1] for x in self]), (None, -1, -1)
331
332
333class Markup(unicode):
334    """Marks a string as being safe for inclusion in HTML/XML output without
335    needing to be escaped.
336    """
337    __slots__ = []
338
339    def __new__(cls, text='', *args):
340        if args:
341            text %= tuple(map(escape, args))
342        return unicode.__new__(cls, text)
343
344    def __add__(self, other):
345        return Markup(unicode(self) + unicode(escape(other)))
346
347    def __radd__(self, other):
348        return Markup(unicode(escape(other)) + unicode(self))
349
350    def __mod__(self, args):
351        if not isinstance(args, (list, tuple)):
352            args = [args]
353        return Markup(unicode.__mod__(self, tuple(map(escape, args))))
354
355    def __mul__(self, num):
356        return Markup(unicode(self) * num)
357
358    def __rmul__(self, num):
359        return Markup(num * unicode(self))
360
361    def __repr__(self):
362        return '<%s %r>' % (self.__class__.__name__, unicode(self))
363
364    def join(self, seq, escape_quotes=True):
365        """Return a `Markup` object which is the concatenation of the strings
366        in the given sequence, where this `Markup` object is the separator
367        between the joined elements.
368       
369        Any element in the sequence that is not a `Markup` instance is
370        automatically escaped.
371       
372        :param seq: the sequence of strings to join
373        :param escape_quotes: whether double quote characters in the elements
374                              should be escaped
375        :return: the joined `Markup` object
376        :see: `escape`
377        """
378        return Markup(unicode(self).join([escape(item, quotes=escape_quotes)
379                                          for item in seq]))
380
381    def escape(cls, text, quotes=True):
382        """Create a Markup instance from a string and escape special characters
383        it may contain (<, >, & and \").
384       
385        >>> escape('"1 < 2"')
386        <Markup u'&#34;1 &lt; 2&#34;'>
387       
388        If the `quotes` parameter is set to `False`, the \" character is left
389        as is. Escaping quotes is generally only required for strings that are
390        to be used in attribute values.
391       
392        >>> escape('"1 < 2"', quotes=False)
393        <Markup u'"1 &lt; 2"'>
394       
395        :param text: the text to escape
396        :param quotes: if ``True``, double quote characters are escaped in
397                       addition to the other special characters
398        :return: the escaped `Markup` string
399        :see: `genshi.core.escape`
400        """
401        if not text:
402            return cls()
403        if type(text) is cls:
404            return text
405        text = unicode(text).replace('&', '&amp;') \
406                            .replace('<', '&lt;') \
407                            .replace('>', '&gt;')
408        if quotes:
409            text = text.replace('"', '&#34;')
410        return cls(text)
411    escape = classmethod(escape)
412
413    def unescape(self):
414        """Reverse-escapes &, <, >, and \" and returns a `unicode` object.
415       
416        >>> Markup('1 &lt; 2').unescape()
417        u'1 < 2'
418       
419        :see: `genshi.core.unescape`
420        """
421        if not self:
422            return u''
423        return unicode(self).replace('&#34;', '"') \
424                            .replace('&gt;', '>') \
425                            .replace('&lt;', '<') \
426                            .replace('&amp;', '&')
427
428    def stripentities(self, keepxmlentities=False):
429        """Return a copy of the text with any character or numeric entities
430        replaced by the equivalent UTF-8 characters.
431       
432        If the `keepxmlentities` parameter is provided and evaluates to `True`,
433        the core XML entities (``&amp;``, ``&apos;``, ``&gt;``, ``&lt;`` and
434        ``&quot;``) are not stripped.
435       
436        :see: `genshi.util.stripentities`
437        """
438        return Markup(stripentities(self, keepxmlentities=keepxmlentities))
439
440    def striptags(self):
441        """Return a copy of the text with all XML/HTML tags removed.
442       
443        :see: `genshi.util.striptags`
444        """
445        return Markup(striptags(self))
446
447
448escape = Markup.escape
449
450def unescape(text):
451    """Reverse-escapes &, <, >, and \" and returns a `unicode` object.
452   
453    >>> unescape(Markup('1 &lt; 2'))
454    u'1 < 2'
455   
456    If the provided `text` object is not a `Markup` instance, the text is
457    returned as-is.
458   
459    >>> unescape('1 &lt; 2')
460    '1 &lt; 2'
461   
462    :param text: the text to unescape
463    """
464    if not isinstance(text, Markup):
465        return text
466    return text.unescape()
467
468
469class Namespace(object):
470    """Utility class creating and testing elements with a namespace.
471   
472    Internally, namespace URIs are encoded in the `QName` of any element or
473    attribute, the namespace URI being enclosed in curly braces. This class
474    helps create and test these strings.
475   
476    A `Namespace` object is instantiated with the namespace URI.
477   
478    >>> html = Namespace('http://www.w3.org/1999/xhtml')
479    >>> html
480    <Namespace "http://www.w3.org/1999/xhtml">
481    >>> html.uri
482    u'http://www.w3.org/1999/xhtml'
483   
484    The `Namespace` object can than be used to generate `QName` objects with
485    that namespace:
486   
487    >>> html.body
488    QName(u'http://www.w3.org/1999/xhtml}body')
489    >>> html.body.localname
490    u'body'
491    >>> html.body.namespace
492    u'http://www.w3.org/1999/xhtml'
493   
494    The same works using item access notation, which is useful for element or
495    attribute names that are not valid Python identifiers:
496   
497    >>> html['body']
498    QName(u'http://www.w3.org/1999/xhtml}body')
499   
500    A `Namespace` object can also be used to test whether a specific `QName`
501    belongs to that namespace using the ``in`` operator:
502   
503    >>> qname = html.body
504    >>> qname in html
505    True
506    >>> qname in Namespace('http://www.w3.org/2002/06/xhtml2')
507    False
508    """
509    def __new__(cls, uri):
510        if type(uri) is cls:
511            return uri
512        return object.__new__(cls, uri)
513
514    def __getnewargs__(self):
515        return (self.uri,)
516
517    def __getstate__(self):
518        return self.uri
519
520    def __setstate__(self, uri):
521        self.uri = uri
522
523    def __init__(self, uri):
524        self.uri = unicode(uri)
525
526    def __contains__(self, qname):
527        return qname.namespace == self.uri
528
529    def __ne__(self, other):
530        return not self == other
531
532    def __eq__(self, other):
533        if isinstance(other, Namespace):
534            return self.uri == other.uri
535        return self.uri == other
536
537    def __getitem__(self, name):
538        return QName(self.uri + u'}' + name)
539    __getattr__ = __getitem__
540
541    def __repr__(self):
542        return '<Namespace "%s">' % self.uri
543
544    def __str__(self):
545        return self.uri.encode('utf-8')
546
547    def __unicode__(self):
548        return self.uri
549
550
551# The namespace used by attributes such as xml:lang and xml:space
552XML_NAMESPACE = Namespace('http://www.w3.org/XML/1998/namespace')
553
554
555class QName(unicode):
556    """A qualified element or attribute name.
557   
558    The unicode value of instances of this class contains the qualified name of
559    the element or attribute, in the form ``{namespace}localname``. The namespace
560    URI can be obtained through the additional `namespace` attribute, while the
561    local name can be accessed through the `localname` attribute.
562   
563    >>> qname = QName('foo')
564    >>> qname
565    QName(u'foo')
566    >>> qname.localname
567    u'foo'
568    >>> qname.namespace
569   
570    >>> qname = QName('http://www.w3.org/1999/xhtml}body')
571    >>> qname
572    QName(u'http://www.w3.org/1999/xhtml}body')
573    >>> qname.localname
574    u'body'
575    >>> qname.namespace
576    u'http://www.w3.org/1999/xhtml'
577    """
578    __slots__ = ['namespace', 'localname']
579
580    def __new__(cls, qname):
581        if type(qname) is cls:
582            return qname
583
584        parts = qname.split(u'}', 1)
585        if len(parts) > 1:
586            self = unicode.__new__(cls, u'{%s' % qname)
587            self.namespace, self.localname = map(unicode, parts)
588        else:
589            self = unicode.__new__(cls, qname)
590            self.namespace, self.localname = None, unicode(qname)
591        return self
592
593    def __getnewargs__(self):
594        return (self.lstrip('{'),)
595
596    def __repr__(self):
597        return 'QName(%s)' % unicode.__repr__(self.lstrip('{'))
Note: See TracBrowser for help on using the repository browser.