Edgewall Software

source: tags/0.6.1/genshi/output.py

Last change on this file was 1165, checked in by hodgestar, 12 years ago

Merge r1163 and r1164 from trunk (fix Markup event caching issue in serializers, issue #429).

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