Edgewall Software

source: branches/stable/0.5.x/genshi/output.py

Last change on this file was 869, checked in by cmlenz, 15 years ago

Preparing for 0.5 release.

  • Property svn:eol-style set to native
File size: 27.4 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2008 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"""This module provides different kinds of serialization methods for XML event
15streams.
16"""
17
18from itertools import chain
19try:
20    frozenset
21except NameError:
22    from sets import ImmutableSet as frozenset
23import re
24
25from genshi.core import escape, Attrs, Markup, Namespace, QName, StreamEventKind
26from genshi.core import START, END, TEXT, XML_DECL, DOCTYPE, START_NS, END_NS, \
27                        START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE
28
29__all__ = ['encode', 'get_serializer', 'DocType', 'XMLSerializer',
30           'XHTMLSerializer', 'HTMLSerializer', 'TextSerializer']
31__docformat__ = 'restructuredtext en'
32
33def encode(iterator, method='xml', encoding='utf-8', out=None):
34    """Encode serializer output into a string.
35   
36    :param iterator: the iterator returned from serializing a stream (basically
37                     any iterator that yields unicode objects)
38    :param method: the serialization method; determines how characters not
39                   representable in the specified encoding are treated
40    :param encoding: how the output string should be encoded; if set to `None`,
41                     this method returns a `unicode` object
42    :param out: a file-like object that the output should be written to
43                instead of being returned as one big string; note that if
44                this is a file or socket (or similar), the `encoding` must
45                not be `None` (that is, the output must be encoded)
46    :return: a `str` or `unicode` object (depending on the `encoding`
47             parameter), or `None` if the `out` parameter is provided
48   
49    :since: version 0.4.1
50    :note: Changed in 0.5: added the `out` parameter
51    """
52    if encoding is not None:
53        errors = 'replace'
54        if method != 'text' and not isinstance(method, TextSerializer):
55            errors = 'xmlcharrefreplace'
56        _encode = lambda string: string.encode(encoding, errors)
57    else:
58        _encode = lambda string: string
59    if out is None:
60        return _encode(u''.join(list(iterator)))
61    for chunk in iterator:
62        out.write(_encode(chunk))
63
64def get_serializer(method='xml', **kwargs):
65    """Return a serializer object for the given method.
66   
67    :param method: the serialization method; can be either "xml", "xhtml",
68                   "html", "text", or a custom serializer class
69
70    Any additional keyword arguments are passed to the serializer, and thus
71    depend on the `method` parameter value.
72   
73    :see: `XMLSerializer`, `XHTMLSerializer`, `HTMLSerializer`, `TextSerializer`
74    :since: version 0.4.1
75    """
76    if isinstance(method, basestring):
77        method = {'xml':   XMLSerializer,
78                  'xhtml': XHTMLSerializer,
79                  'html':  HTMLSerializer,
80                  'text':  TextSerializer}[method.lower()]
81    return method(**kwargs)
82
83
84class DocType(object):
85    """Defines a number of commonly used DOCTYPE declarations as constants."""
86
87    HTML_STRICT = (
88        'html', '-//W3C//DTD HTML 4.01//EN',
89        'http://www.w3.org/TR/html4/strict.dtd'
90    )
91    HTML_TRANSITIONAL = (
92        'html', '-//W3C//DTD HTML 4.01 Transitional//EN',
93        'http://www.w3.org/TR/html4/loose.dtd'
94    )
95    HTML_FRAMESET = (
96        'html', '-//W3C//DTD HTML 4.01 Frameset//EN',
97        'http://www.w3.org/TR/html4/frameset.dtd'
98    )
99    HTML = HTML_STRICT
100
101    HTML5 = ('html', None, None)
102
103    XHTML_STRICT = (
104        'html', '-//W3C//DTD XHTML 1.0 Strict//EN',
105        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
106    )
107    XHTML_TRANSITIONAL = (
108        'html', '-//W3C//DTD XHTML 1.0 Transitional//EN',
109        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
110    )
111    XHTML_FRAMESET = (
112        'html', '-//W3C//DTD XHTML 1.0 Frameset//EN',
113        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd'
114    )
115    XHTML = XHTML_STRICT
116
117    XHTML11 = (
118        'html', '-//W3C//DTD XHTML 1.1//EN',
119        'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
120    )
121
122    SVG_FULL = (
123        'svg', '-//W3C//DTD SVG 1.1//EN',
124        'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
125    )
126    SVG_BASIC = (
127        'svg', '-//W3C//DTD SVG Basic 1.1//EN',
128        'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd'
129    )
130    SVG_TINY = (
131        'svg', '-//W3C//DTD SVG Tiny 1.1//EN',
132        'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd'
133    )
134    SVG = SVG_FULL
135
136    def get(cls, name):
137        """Return the ``(name, pubid, sysid)`` tuple of the ``DOCTYPE``
138        declaration for the specified name.
139       
140        The following names are recognized in this version:
141         * "html" or "html-strict" for the HTML 4.01 strict DTD
142         * "html-transitional" for the HTML 4.01 transitional DTD
143         * "html-frameset" for the HTML 4.01 frameset DTD
144         * "html5" for the ``DOCTYPE`` proposed for HTML5
145         * "xhtml" or "xhtml-strict" for the XHTML 1.0 strict DTD
146         * "xhtml-transitional" for the XHTML 1.0 transitional DTD
147         * "xhtml-frameset" for the XHTML 1.0 frameset DTD
148         * "xhtml11" for the XHTML 1.1 DTD
149         * "svg" or "svg-full" for the SVG 1.1 DTD
150         * "svg-basic" for the SVG Basic 1.1 DTD
151         * "svg-tiny" for the SVG Tiny 1.1 DTD
152       
153        :param name: the name of the ``DOCTYPE``
154        :return: the ``(name, pubid, sysid)`` tuple for the requested
155                 ``DOCTYPE``, or ``None`` if the name is not recognized
156        :since: version 0.4.1
157        """
158        return {
159            'html': cls.HTML, 'html-strict': cls.HTML_STRICT,
160            'html-transitional': DocType.HTML_TRANSITIONAL,
161            'html-frameset': DocType.HTML_FRAMESET,
162            'html5': cls.HTML5,
163            'xhtml': cls.XHTML, 'xhtml-strict': cls.XHTML_STRICT,
164            'xhtml-transitional': cls.XHTML_TRANSITIONAL,
165            'xhtml-frameset': cls.XHTML_FRAMESET,
166            'xhtml11': cls.XHTML11,
167            'svg': cls.SVG, 'svg-full': cls.SVG_FULL,
168            'svg-basic': cls.SVG_BASIC,
169            'svg-tiny': cls.SVG_TINY
170        }.get(name.lower())
171    get = classmethod(get)
172
173
174class XMLSerializer(object):
175    """Produces XML text from an event stream.
176   
177    >>> from genshi.builder import tag
178    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
179    >>> print ''.join(XMLSerializer()(elem.generate()))
180    <div><a href="foo"/><br/><hr noshade="True"/></div>
181    """
182
183    _PRESERVE_SPACE = frozenset()
184
185    def __init__(self, doctype=None, strip_whitespace=True,
186                 namespace_prefixes=None):
187        """Initialize the XML serializer.
188       
189        :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
190                        DOCTYPE declaration that should be included at the top
191                        of the generated output, or the name of a DOCTYPE as
192                        defined in `DocType.get`
193        :param strip_whitespace: whether extraneous whitespace should be
194                                 stripped from the output
195        :note: Changed in 0.4.2: The  `doctype` parameter can now be a string.
196        """
197        self.filters = [EmptyTagFilter()]
198        if strip_whitespace:
199            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
200        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
201        if doctype:
202            self.filters.append(DocTypeInserter(doctype))
203
204    def __call__(self, stream):
205        have_decl = have_doctype = False
206        in_cdata = False
207
208        for filter_ in self.filters:
209            stream = filter_(stream)
210        for kind, data, pos in stream:
211
212            if kind is START or kind is EMPTY:
213                tag, attrib = data
214                buf = ['<', tag]
215                for attr, value in attrib:
216                    buf += [' ', attr, '="', escape(value), '"']
217                buf.append(kind is EMPTY and '/>' or '>')
218                yield Markup(u''.join(buf))
219
220            elif kind is END:
221                yield Markup('</%s>' % data)
222
223            elif kind is TEXT:
224                if in_cdata:
225                    yield data
226                else:
227                    yield escape(data, quotes=False)
228
229            elif kind is COMMENT:
230                yield Markup('<!--%s-->' % data)
231
232            elif kind is XML_DECL and not have_decl:
233                version, encoding, standalone = data
234                buf = ['<?xml version="%s"' % version]
235                if encoding:
236                    buf.append(' encoding="%s"' % encoding)
237                if standalone != -1:
238                    standalone = standalone and 'yes' or 'no'
239                    buf.append(' standalone="%s"' % standalone)
240                buf.append('?>\n')
241                yield Markup(u''.join(buf))
242                have_decl = True
243
244            elif kind is DOCTYPE and not have_doctype:
245                name, pubid, sysid = data
246                buf = ['<!DOCTYPE %s']
247                if pubid:
248                    buf.append(' PUBLIC "%s"')
249                elif sysid:
250                    buf.append(' SYSTEM')
251                if sysid:
252                    buf.append(' "%s"')
253                buf.append('>\n')
254                yield Markup(u''.join(buf)) % filter(None, data)
255                have_doctype = True
256
257            elif kind is START_CDATA:
258                yield Markup('<![CDATA[')
259                in_cdata = True
260
261            elif kind is END_CDATA:
262                yield Markup(']]>')
263                in_cdata = False
264
265            elif kind is PI:
266                yield Markup('<?%s %s?>' % data)
267
268
269class XHTMLSerializer(XMLSerializer):
270    """Produces XHTML text from an event stream.
271   
272    >>> from genshi.builder import tag
273    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
274    >>> print ''.join(XHTMLSerializer()(elem.generate()))
275    <div><a href="foo"></a><br /><hr noshade="noshade" /></div>
276    """
277
278    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
279                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
280                              'param'])
281    _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
282                                'defer', 'disabled', 'ismap', 'multiple',
283                                'nohref', 'noresize', 'noshade', 'nowrap'])
284    _PRESERVE_SPACE = frozenset([
285        QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'),
286        QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea')
287    ])
288
289    def __init__(self, doctype=None, strip_whitespace=True,
290                 namespace_prefixes=None, drop_xml_decl=True):
291        super(XHTMLSerializer, self).__init__(doctype, False)
292        self.filters = [EmptyTagFilter()]
293        if strip_whitespace:
294            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
295        namespace_prefixes = namespace_prefixes or {}
296        namespace_prefixes['http://www.w3.org/1999/xhtml'] = ''
297        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
298        if doctype:
299            self.filters.append(DocTypeInserter(doctype))
300        self.drop_xml_decl = drop_xml_decl
301
302    def __call__(self, stream):
303        boolean_attrs = self._BOOLEAN_ATTRS
304        empty_elems = self._EMPTY_ELEMS
305        drop_xml_decl = self.drop_xml_decl
306        have_decl = have_doctype = False
307        in_cdata = False
308
309        for filter_ in self.filters:
310            stream = filter_(stream)
311        for kind, data, pos in stream:
312
313            if kind is START or kind is EMPTY:
314                tag, attrib = data
315                buf = ['<', tag]
316                for attr, value in attrib:
317                    if attr in boolean_attrs:
318                        value = attr
319                    elif attr == u'xml:lang' and u'lang' not in attrib:
320                        buf += [' lang="', escape(value), '"']
321                    elif attr == u'xml:space':
322                        continue
323                    buf += [' ', attr, '="', escape(value), '"']
324                if kind is EMPTY:
325                    if tag in empty_elems:
326                        buf.append(' />')
327                    else:
328                        buf.append('></%s>' % tag)
329                else:
330                    buf.append('>')
331                yield Markup(u''.join(buf))
332
333            elif kind is END:
334                yield Markup('</%s>' % data)
335
336            elif kind is TEXT:
337                if in_cdata:
338                    yield data
339                else:
340                    yield escape(data, quotes=False)
341
342            elif kind is COMMENT:
343                yield Markup('<!--%s-->' % data)
344
345            elif kind is DOCTYPE and not have_doctype:
346                name, pubid, sysid = data
347                buf = ['<!DOCTYPE %s']
348                if pubid:
349                    buf.append(' PUBLIC "%s"')
350                elif sysid:
351                    buf.append(' SYSTEM')
352                if sysid:
353                    buf.append(' "%s"')
354                buf.append('>\n')
355                yield Markup(u''.join(buf)) % filter(None, data)
356                have_doctype = True
357
358            elif kind is XML_DECL and not have_decl and not drop_xml_decl:
359                version, encoding, standalone = data
360                buf = ['<?xml version="%s"' % version]
361                if encoding:
362                    buf.append(' encoding="%s"' % encoding)
363                if standalone != -1:
364                    standalone = standalone and 'yes' or 'no'
365                    buf.append(' standalone="%s"' % standalone)
366                buf.append('?>\n')
367                yield Markup(u''.join(buf))
368                have_decl = True
369
370            elif kind is START_CDATA:
371                yield Markup('<![CDATA[')
372                in_cdata = True
373
374            elif kind is END_CDATA:
375                yield Markup(']]>')
376                in_cdata = False
377
378            elif kind is PI:
379                yield Markup('<?%s %s?>' % data)
380
381
382class HTMLSerializer(XHTMLSerializer):
383    """Produces HTML text from an event stream.
384   
385    >>> from genshi.builder import tag
386    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
387    >>> print ''.join(HTMLSerializer()(elem.generate()))
388    <div><a href="foo"></a><br><hr noshade></div>
389    """
390
391    _NOESCAPE_ELEMS = frozenset([
392        QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
393        QName('style'), QName('http://www.w3.org/1999/xhtml}style')
394    ])
395
396    def __init__(self, doctype=None, strip_whitespace=True):
397        """Initialize the HTML serializer.
398       
399        :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
400                        DOCTYPE declaration that should be included at the top
401                        of the generated output
402        :param strip_whitespace: whether extraneous whitespace should be
403                                 stripped from the output
404        """
405        super(HTMLSerializer, self).__init__(doctype, False)
406        self.filters = [EmptyTagFilter()]
407        if strip_whitespace:
408            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
409                                                 self._NOESCAPE_ELEMS))
410        self.filters.append(NamespaceFlattener(prefixes={
411            'http://www.w3.org/1999/xhtml': ''
412        }))
413        if doctype:
414            self.filters.append(DocTypeInserter(doctype))
415
416    def __call__(self, stream):
417        boolean_attrs = self._BOOLEAN_ATTRS
418        empty_elems = self._EMPTY_ELEMS
419        noescape_elems = self._NOESCAPE_ELEMS
420        have_doctype = False
421        noescape = False
422
423        for filter_ in self.filters:
424            stream = filter_(stream)
425        for kind, data, pos in stream:
426
427            if kind is START or kind is EMPTY:
428                tag, attrib = data
429                buf = ['<', tag]
430                for attr, value in attrib:
431                    if attr in boolean_attrs:
432                        if value:
433                            buf += [' ', attr]
434                    elif ':' in attr:
435                        if attr == 'xml:lang' and u'lang' not in attrib:
436                            buf += [' lang="', escape(value), '"']
437                    elif attr != 'xmlns':
438                        buf += [' ', attr, '="', escape(value), '"']
439                buf.append('>')
440                if kind is EMPTY:
441                    if tag not in empty_elems:
442                        buf.append('</%s>' % tag)
443                yield Markup(u''.join(buf))
444                if tag in noescape_elems:
445                    noescape = True
446
447            elif kind is END:
448                yield Markup('</%s>' % data)
449                noescape = False
450
451            elif kind is TEXT:
452                if noescape:
453                    yield data
454                else:
455                    yield escape(data, quotes=False)
456
457            elif kind is COMMENT:
458                yield Markup('<!--%s-->' % data)
459
460            elif kind is DOCTYPE and not have_doctype:
461                name, pubid, sysid = data
462                buf = ['<!DOCTYPE %s']
463                if pubid:
464                    buf.append(' PUBLIC "%s"')
465                elif sysid:
466                    buf.append(' SYSTEM')
467                if sysid:
468                    buf.append(' "%s"')
469                buf.append('>\n')
470                yield Markup(u''.join(buf)) % filter(None, data)
471                have_doctype = True
472
473            elif kind is PI:
474                yield Markup('<?%s %s?>' % data)
475
476
477class TextSerializer(object):
478    """Produces plain text from an event stream.
479   
480    Only text events are included in the output. Unlike the other serializer,
481    special XML characters are not escaped:
482   
483    >>> from genshi.builder import tag
484    >>> elem = tag.div(tag.a('<Hello!>', href='foo'), tag.br)
485    >>> print elem
486    <div><a href="foo">&lt;Hello!&gt;</a><br/></div>
487    >>> print ''.join(TextSerializer()(elem.generate()))
488    <Hello!>
489
490    If text events contain literal markup (instances of the `Markup` class),
491    that markup is by default passed through unchanged:
492   
493    >>> elem = tag.div(Markup('<a href="foo">Hello &amp; Bye!</a><br/>'))
494    >>> print elem.generate().render(TextSerializer)
495    <a href="foo">Hello &amp; Bye!</a><br/>
496   
497    You can use the ``strip_markup`` to change this behavior, so that tags and
498    entities are stripped from the output (or in the case of entities,
499    replaced with the equivalent character):
500
501    >>> print elem.generate().render(TextSerializer, strip_markup=True)
502    Hello & Bye!
503    """
504
505    def __init__(self, strip_markup=False):
506        """Create the serializer.
507       
508        :param strip_markup: whether markup (tags and encoded characters) found
509                             in the text should be removed
510        """
511        self.strip_markup = strip_markup
512
513    def __call__(self, stream):
514        strip_markup = self.strip_markup
515        for event in stream:
516            if event[0] is TEXT:
517                data = event[1]
518                if strip_markup and type(data) is Markup:
519                    data = data.striptags().stripentities()
520                yield unicode(data)
521
522
523class EmptyTagFilter(object):
524    """Combines `START` and `STOP` events into `EMPTY` events for elements that
525    have no contents.
526    """
527
528    EMPTY = StreamEventKind('EMPTY')
529
530    def __call__(self, stream):
531        prev = (None, None, None)
532        for ev in stream:
533            if prev[0] is START:
534                if ev[0] is END:
535                    prev = EMPTY, prev[1], prev[2]
536                    yield prev
537                    continue
538                else:
539                    yield prev
540            if ev[0] is not START:
541                yield ev
542            prev = ev
543
544
545EMPTY = EmptyTagFilter.EMPTY
546
547
548class NamespaceFlattener(object):
549    r"""Output stream filter that removes namespace information from the stream,
550    instead adding namespace attributes and prefixes as needed.
551   
552    :param prefixes: optional mapping of namespace URIs to prefixes
553   
554    >>> from genshi.input import XML
555    >>> xml = XML('''<doc xmlns="NS1" xmlns:two="NS2">
556    ...   <two:item/>
557    ... </doc>''')
558    >>> for kind, data, pos in NamespaceFlattener()(xml):
559    ...     print kind, repr(data)
560    START (u'doc', Attrs([(u'xmlns', u'NS1'), (u'xmlns:two', u'NS2')]))
561    TEXT u'\n  '
562    START (u'two:item', Attrs())
563    END u'two:item'
564    TEXT u'\n'
565    END u'doc'
566    """
567
568    def __init__(self, prefixes=None):
569        self.prefixes = {XML_NAMESPACE.uri: 'xml'}
570        if prefixes is not None:
571            self.prefixes.update(prefixes)
572
573    def __call__(self, stream):
574        prefixes = dict([(v, [k]) for k, v in self.prefixes.items()])
575        namespaces = {XML_NAMESPACE.uri: ['xml']}
576        def _push_ns(prefix, uri):
577            namespaces.setdefault(uri, []).append(prefix)
578            prefixes.setdefault(prefix, []).append(uri)
579
580        ns_attrs = []
581        _push_ns_attr = ns_attrs.append
582        def _make_ns_attr(prefix, uri):
583            return u'xmlns%s' % (prefix and ':%s' % prefix or ''), uri
584
585        def _gen_prefix():
586            val = 0
587            while 1:
588                val += 1
589                yield 'ns%d' % val
590        _gen_prefix = _gen_prefix().next
591
592        for kind, data, pos in stream:
593
594            if kind is START or kind is EMPTY:
595                tag, attrs = data
596
597                tagname = tag.localname
598                tagns = tag.namespace
599                if tagns:
600                    if tagns in namespaces:
601                        prefix = namespaces[tagns][-1]
602                        if prefix:
603                            tagname = u'%s:%s' % (prefix, tagname)
604                    else:
605                        _push_ns_attr((u'xmlns', tagns))
606                        _push_ns('', tagns)
607
608                new_attrs = []
609                for attr, value in attrs:
610                    attrname = attr.localname
611                    attrns = attr.namespace
612                    if attrns:
613                        if attrns not in namespaces:
614                            prefix = _gen_prefix()
615                            _push_ns(prefix, attrns)
616                            _push_ns_attr(('xmlns:%s' % prefix, attrns))
617                        else:
618                            prefix = namespaces[attrns][-1]
619                        if prefix:
620                            attrname = u'%s:%s' % (prefix, attrname)
621                    new_attrs.append((attrname, value))
622
623                yield kind, (tagname, Attrs(ns_attrs + new_attrs)), pos
624                del ns_attrs[:]
625
626            elif kind is END:
627                tagname = data.localname
628                tagns = data.namespace
629                if tagns:
630                    prefix = namespaces[tagns][-1]
631                    if prefix:
632                        tagname = u'%s:%s' % (prefix, tagname)
633                yield kind, tagname, pos
634
635            elif kind is START_NS:
636                prefix, uri = data
637                if uri not in namespaces:
638                    prefix = prefixes.get(uri, [prefix])[-1]
639                    _push_ns_attr(_make_ns_attr(prefix, uri))
640                _push_ns(prefix, uri)
641
642            elif kind is END_NS:
643                if data in prefixes:
644                    uris = prefixes.get(data)
645                    uri = uris.pop()
646                    if not uris:
647                        del prefixes[data]
648                    if uri not in uris or uri != uris[-1]:
649                        uri_prefixes = namespaces[uri]
650                        uri_prefixes.pop()
651                        if not uri_prefixes:
652                            del namespaces[uri]
653                    if ns_attrs:
654                        attr = _make_ns_attr(data, uri)
655                        if attr in ns_attrs:
656                            ns_attrs.remove(attr)
657
658            else:
659                yield kind, data, pos
660
661
662class WhitespaceFilter(object):
663    """A filter that removes extraneous ignorable white space from the
664    stream.
665    """
666
667    def __init__(self, preserve=None, noescape=None):
668        """Initialize the filter.
669       
670        :param preserve: a set or sequence of tag names for which white-space
671                         should be preserved
672        :param noescape: a set or sequence of tag names for which text content
673                         should not be escaped
674       
675        The `noescape` set is expected to refer to elements that cannot contain
676        further child elements (such as ``<style>`` or ``<script>`` in HTML
677        documents).
678        """
679        if preserve is None:
680            preserve = []
681        self.preserve = frozenset(preserve)
682        if noescape is None:
683            noescape = []
684        self.noescape = frozenset(noescape)
685
686    def __call__(self, stream, ctxt=None, space=XML_NAMESPACE['space'],
687                 trim_trailing_space=re.compile('[ \t]+(?=\n)').sub,
688                 collapse_lines=re.compile('\n{2,}').sub):
689        mjoin = Markup('').join
690        preserve_elems = self.preserve
691        preserve = 0
692        noescape_elems = self.noescape
693        noescape = False
694
695        textbuf = []
696        push_text = textbuf.append
697        pop_text = textbuf.pop
698        for kind, data, pos in chain(stream, [(None, None, None)]):
699
700            if kind is TEXT:
701                if noescape:
702                    data = Markup(data)
703                push_text(data)
704            else:
705                if textbuf:
706                    if len(textbuf) > 1:
707                        text = mjoin(textbuf, escape_quotes=False)
708                        del textbuf[:]
709                    else:
710                        text = escape(pop_text(), quotes=False)
711                    if not preserve:
712                        text = collapse_lines('\n', trim_trailing_space('', text))
713                    yield TEXT, Markup(text), pos
714
715                if kind is START:
716                    tag, attrs = data
717                    if preserve or (tag in preserve_elems or
718                                    attrs.get(space) == 'preserve'):
719                        preserve += 1
720                    if not noescape and tag in noescape_elems:
721                        noescape = True
722
723                elif kind is END:
724                    noescape = False
725                    if preserve:
726                        preserve -= 1
727
728                elif kind is START_CDATA:
729                    noescape = True
730
731                elif kind is END_CDATA:
732                    noescape = False
733
734                if kind:
735                    yield kind, data, pos
736
737
738class DocTypeInserter(object):
739    """A filter that inserts the DOCTYPE declaration in the correct location,
740    after the XML declaration.
741    """
742    def __init__(self, doctype):
743        """Initialize the filter.
744
745        :param doctype: DOCTYPE as a string or DocType object.
746        """
747        if isinstance(doctype, basestring):
748            doctype = DocType.get(doctype)
749        self.doctype_event = (DOCTYPE, doctype, (None, -1, -1))
750
751    def __call__(self, stream):
752        doctype_inserted = False
753        for kind, data, pos in stream:
754            if not doctype_inserted:
755                doctype_inserted = True
756                if kind is XML_DECL:
757                    yield (kind, data, pos)
758                    yield self.doctype_event
759                    continue
760                yield self.doctype_event
761
762            yield (kind, data, pos)
763
764        if not doctype_inserted:
765            yield self.doctype_event
Note: See TracBrowser for help on using the repository browser.