Edgewall Software

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

Last change on this file was 1262, checked in by hodgestar, 10 years ago

Merge r1260 from trunk (add missing boolean attributes to XHTML and HTML serializers).

  • Property svn:eol-style set to native
File size: 30.7 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                                'autofocus', 'readonly', 'required',
321                                'formnovalidate'])
322    _PRESERVE_SPACE = frozenset([
323        QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'),
324        QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea')
325    ])
326
327    def __init__(self, doctype=None, strip_whitespace=True,
328                 namespace_prefixes=None, drop_xml_decl=True, cache=True):
329        super(XHTMLSerializer, self).__init__(doctype, False)
330        self.filters = [EmptyTagFilter()]
331        if strip_whitespace:
332            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
333        namespace_prefixes = namespace_prefixes or {}
334        namespace_prefixes['http://www.w3.org/1999/xhtml'] = ''
335        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
336                                               cache=cache))
337        if doctype:
338            self.filters.append(DocTypeInserter(doctype))
339        self.drop_xml_decl = drop_xml_decl
340        self.cache = cache
341
342    def __call__(self, stream):
343        boolean_attrs = self._BOOLEAN_ATTRS
344        empty_elems = self._EMPTY_ELEMS
345        drop_xml_decl = self.drop_xml_decl
346        have_decl = have_doctype = False
347        in_cdata = False
348        _emit, _get = self._prepare_cache()
349
350        for filter_ in self.filters:
351            stream = filter_(stream)
352        for kind, data, pos in stream:
353            if kind is TEXT and isinstance(data, Markup):
354                yield data
355                continue
356            cached = _get((kind, data))
357            if cached is not None:
358                yield cached
359
360            elif kind is START or kind is EMPTY:
361                tag, attrib = data
362                buf = ['<', tag]
363                for attr, value in attrib:
364                    if attr in boolean_attrs:
365                        value = attr
366                    elif attr == 'xml:lang' and 'lang' not in attrib:
367                        buf += [' lang="', escape(value), '"']
368                    elif attr == 'xml:space':
369                        continue
370                    buf += [' ', attr, '="', escape(value), '"']
371                if kind is EMPTY:
372                    if tag in empty_elems:
373                        buf.append(' />')
374                    else:
375                        buf.append('></%s>' % tag)
376                else:
377                    buf.append('>')
378                yield _emit(kind, data, Markup(''.join(buf)))
379
380            elif kind is END:
381                yield _emit(kind, data, Markup('</%s>' % data))
382
383            elif kind is TEXT:
384                if in_cdata:
385                    yield _emit(kind, data, data)
386                else:
387                    yield _emit(kind, data, escape(data, quotes=False))
388
389            elif kind is COMMENT:
390                yield _emit(kind, data, Markup('<!--%s-->' % data))
391
392            elif kind is DOCTYPE and not have_doctype:
393                name, pubid, sysid = data
394                buf = ['<!DOCTYPE %s']
395                if pubid:
396                    buf.append(' PUBLIC "%s"')
397                elif sysid:
398                    buf.append(' SYSTEM')
399                if sysid:
400                    buf.append(' "%s"')
401                buf.append('>\n')
402                yield Markup(''.join(buf)) % tuple([p for p in data if p])
403                have_doctype = True
404
405            elif kind is XML_DECL and not have_decl and not drop_xml_decl:
406                version, encoding, standalone = data
407                buf = ['<?xml version="%s"' % version]
408                if encoding:
409                    buf.append(' encoding="%s"' % encoding)
410                if standalone != -1:
411                    standalone = standalone and 'yes' or 'no'
412                    buf.append(' standalone="%s"' % standalone)
413                buf.append('?>\n')
414                yield Markup(''.join(buf))
415                have_decl = True
416
417            elif kind is START_CDATA:
418                yield Markup('<![CDATA[')
419                in_cdata = True
420
421            elif kind is END_CDATA:
422                yield Markup(']]>')
423                in_cdata = False
424
425            elif kind is PI:
426                yield _emit(kind, data, Markup('<?%s %s?>' % data))
427
428
429class HTMLSerializer(XHTMLSerializer):
430    """Produces HTML text from an event stream.
431   
432    >>> from genshi.builder import tag
433    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
434    >>> print(''.join(HTMLSerializer()(elem.generate())))
435    <div><a href="foo"></a><br><hr noshade></div>
436    """
437
438    _NOESCAPE_ELEMS = frozenset([
439        QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
440        QName('style'), QName('http://www.w3.org/1999/xhtml}style')
441    ])
442
443    def __init__(self, doctype=None, strip_whitespace=True, cache=True):
444        """Initialize the HTML serializer.
445       
446        :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
447                        DOCTYPE declaration that should be included at the top
448                        of the generated output
449        :param strip_whitespace: whether extraneous whitespace should be
450                                 stripped from the output
451        :param cache: whether to cache the text output per event, which
452                      improves performance for repetitive markup
453        :note: Changed in 0.6: The `cache` parameter was added
454        """
455        super(HTMLSerializer, self).__init__(doctype, False)
456        self.filters = [EmptyTagFilter()]
457        if strip_whitespace:
458            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
459                                                 self._NOESCAPE_ELEMS))
460        self.filters.append(NamespaceFlattener(prefixes={
461            'http://www.w3.org/1999/xhtml': ''
462        }, cache=cache))
463        if doctype:
464            self.filters.append(DocTypeInserter(doctype))
465        self.cache = True
466
467    def __call__(self, stream):
468        boolean_attrs = self._BOOLEAN_ATTRS
469        empty_elems = self._EMPTY_ELEMS
470        noescape_elems = self._NOESCAPE_ELEMS
471        have_doctype = False
472        noescape = False
473        _emit, _get = self._prepare_cache()
474
475        for filter_ in self.filters:
476            stream = filter_(stream)
477        for kind, data, _ in stream:
478            if kind is TEXT and isinstance(data, Markup):
479                yield data
480                continue
481            output = _get((kind, data))
482            if output is not None:
483                yield output
484                if (kind is START or kind is EMPTY) \
485                        and data[0] in noescape_elems:
486                    noescape = True
487                elif kind is END:
488                    noescape = False
489
490            elif kind is START or kind is EMPTY:
491                tag, attrib = data
492                buf = ['<', tag]
493                for attr, value in attrib:
494                    if attr in boolean_attrs:
495                        if value:
496                            buf += [' ', attr]
497                    elif ':' in attr:
498                        if attr == 'xml:lang' and 'lang' not in attrib:
499                            buf += [' lang="', escape(value), '"']
500                    elif attr != 'xmlns':
501                        buf += [' ', attr, '="', escape(value), '"']
502                buf.append('>')
503                if kind is EMPTY:
504                    if tag not in empty_elems:
505                        buf.append('</%s>' % tag)
506                yield _emit(kind, data, Markup(''.join(buf)))
507                if tag in noescape_elems:
508                    noescape = True
509
510            elif kind is END:
511                yield _emit(kind, data, Markup('</%s>' % data))
512                noescape = False
513
514            elif kind is TEXT:
515                if noescape:
516                    yield _emit(kind, data, data)
517                else:
518                    yield _emit(kind, data, escape(data, quotes=False))
519
520            elif kind is COMMENT:
521                yield _emit(kind, data, Markup('<!--%s-->' % data))
522
523            elif kind is DOCTYPE and not have_doctype:
524                name, pubid, sysid = data
525                buf = ['<!DOCTYPE %s']
526                if pubid:
527                    buf.append(' PUBLIC "%s"')
528                elif sysid:
529                    buf.append(' SYSTEM')
530                if sysid:
531                    buf.append(' "%s"')
532                buf.append('>\n')
533                yield Markup(''.join(buf)) % tuple([p for p in data if p])
534                have_doctype = True
535
536            elif kind is PI:
537                yield _emit(kind, data, Markup('<?%s %s?>' % data))
538
539
540class TextSerializer(object):
541    """Produces plain text from an event stream.
542   
543    Only text events are included in the output. Unlike the other serializer,
544    special XML characters are not escaped:
545   
546    >>> from genshi.builder import tag
547    >>> elem = tag.div(tag.a('<Hello!>', href='foo'), tag.br)
548    >>> print(elem)
549    <div><a href="foo">&lt;Hello!&gt;</a><br/></div>
550    >>> print(''.join(TextSerializer()(elem.generate())))
551    <Hello!>
552
553    If text events contain literal markup (instances of the `Markup` class),
554    that markup is by default passed through unchanged:
555   
556    >>> elem = tag.div(Markup('<a href="foo">Hello &amp; Bye!</a><br/>'))
557    >>> print(elem.generate().render(TextSerializer, encoding=None))
558    <a href="foo">Hello &amp; Bye!</a><br/>
559   
560    You can use the ``strip_markup`` to change this behavior, so that tags and
561    entities are stripped from the output (or in the case of entities,
562    replaced with the equivalent character):
563
564    >>> print(elem.generate().render(TextSerializer, strip_markup=True,
565    ...                              encoding=None))
566    Hello & Bye!
567    """
568
569    def __init__(self, strip_markup=False):
570        """Create the serializer.
571       
572        :param strip_markup: whether markup (tags and encoded characters) found
573                             in the text should be removed
574        """
575        self.strip_markup = strip_markup
576
577    def __call__(self, stream):
578        strip_markup = self.strip_markup
579        for event in stream:
580            if event[0] is TEXT:
581                data = event[1]
582                if strip_markup and type(data) is Markup:
583                    data = data.striptags().stripentities()
584                yield unicode(data)
585
586
587class EmptyTagFilter(object):
588    """Combines `START` and `STOP` events into `EMPTY` events for elements that
589    have no contents.
590    """
591
592    EMPTY = StreamEventKind('EMPTY')
593
594    def __call__(self, stream):
595        prev = (None, None, None)
596        for ev in stream:
597            if prev[0] is START:
598                if ev[0] is END:
599                    prev = EMPTY, prev[1], prev[2]
600                    yield prev
601                    continue
602                else:
603                    yield prev
604            if ev[0] is not START:
605                yield ev
606            prev = ev
607
608
609EMPTY = EmptyTagFilter.EMPTY
610
611
612class NamespaceFlattener(object):
613    r"""Output stream filter that removes namespace information from the stream,
614    instead adding namespace attributes and prefixes as needed.
615   
616    :param prefixes: optional mapping of namespace URIs to prefixes
617   
618    >>> from genshi.input import XML
619    >>> xml = XML('''<doc xmlns="NS1" xmlns:two="NS2">
620    ...   <two:item/>
621    ... </doc>''')
622    >>> for kind, data, pos in NamespaceFlattener()(xml):
623    ...     print('%s %r' % (kind, data))
624    START (u'doc', Attrs([('xmlns', u'NS1'), (u'xmlns:two', u'NS2')]))
625    TEXT u'\n  '
626    START (u'two:item', Attrs())
627    END u'two:item'
628    TEXT u'\n'
629    END u'doc'
630    """
631
632    def __init__(self, prefixes=None, cache=True):
633        self.prefixes = {XML_NAMESPACE.uri: 'xml'}
634        if prefixes is not None:
635            self.prefixes.update(prefixes)
636        self.cache = cache
637
638    def __call__(self, stream):
639        prefixes = dict([(v, [k]) for k, v in self.prefixes.items()])
640        namespaces = {XML_NAMESPACE.uri: ['xml']}
641        _emit, _get, cache = _prepare_cache(self.cache)
642        def _push_ns(prefix, uri):
643            namespaces.setdefault(uri, []).append(prefix)
644            prefixes.setdefault(prefix, []).append(uri)
645            cache.clear()
646        def _pop_ns(prefix):
647            uris = prefixes.get(prefix)
648            uri = uris.pop()
649            if not uris:
650                del prefixes[prefix]
651            if uri not in uris or uri != uris[-1]:
652                uri_prefixes = namespaces[uri]
653                uri_prefixes.pop()
654                if not uri_prefixes:
655                    del namespaces[uri]
656            cache.clear()
657            return uri
658
659        ns_attrs = []
660        _push_ns_attr = ns_attrs.append
661        def _make_ns_attr(prefix, uri):
662            return 'xmlns%s' % (prefix and ':%s' % prefix or ''), uri
663
664        def _gen_prefix():
665            val = 0
666            while 1:
667                val += 1
668                yield 'ns%d' % val
669        _gen_prefix = _gen_prefix().next
670
671        for kind, data, pos in stream:
672            if kind is TEXT and isinstance(data, Markup):
673                yield kind, data, pos
674                continue
675            output = _get((kind, data))
676            if output is not None:
677                yield kind, output, pos
678
679            elif kind is START or kind is EMPTY:
680                tag, attrs = data
681
682                tagname = tag.localname
683                tagns = tag.namespace
684                if tagns:
685                    if tagns in namespaces:
686                        prefix = namespaces[tagns][-1]
687                        if prefix:
688                            tagname = '%s:%s' % (prefix, tagname)
689                    else:
690                        _push_ns_attr(('xmlns', tagns))
691                        _push_ns('', tagns)
692
693                new_attrs = []
694                for attr, value in attrs:
695                    attrname = attr.localname
696                    attrns = attr.namespace
697                    if attrns:
698                        if attrns not in namespaces:
699                            prefix = _gen_prefix()
700                            _push_ns(prefix, attrns)
701                            _push_ns_attr(('xmlns:%s' % prefix, attrns))
702                        else:
703                            prefix = namespaces[attrns][-1]
704                        if prefix:
705                            attrname = '%s:%s' % (prefix, attrname)
706                    new_attrs.append((attrname, value))
707
708                data = _emit(kind, data, (tagname, Attrs(ns_attrs + new_attrs)))
709                yield kind, data, pos
710                del ns_attrs[:]
711
712            elif kind is END:
713                tagname = data.localname
714                tagns = data.namespace
715                if tagns:
716                    prefix = namespaces[tagns][-1]
717                    if prefix:
718                        tagname = '%s:%s' % (prefix, tagname)
719                yield kind, _emit(kind, data, tagname), pos
720
721            elif kind is START_NS:
722                prefix, uri = data
723                if uri not in namespaces:
724                    prefix = prefixes.get(uri, [prefix])[-1]
725                    _push_ns_attr(_make_ns_attr(prefix, uri))
726                _push_ns(prefix, uri)
727
728            elif kind is END_NS:
729                if data in prefixes:
730                    uri = _pop_ns(data)
731                    if ns_attrs:
732                        attr = _make_ns_attr(data, uri)
733                        if attr in ns_attrs:
734                            ns_attrs.remove(attr)
735
736            else:
737                yield kind, data, pos
738
739
740class WhitespaceFilter(object):
741    """A filter that removes extraneous ignorable white space from the
742    stream.
743    """
744
745    def __init__(self, preserve=None, noescape=None):
746        """Initialize the filter.
747       
748        :param preserve: a set or sequence of tag names for which white-space
749                         should be preserved
750        :param noescape: a set or sequence of tag names for which text content
751                         should not be escaped
752       
753        The `noescape` set is expected to refer to elements that cannot contain
754        further child elements (such as ``<style>`` or ``<script>`` in HTML
755        documents).
756        """
757        if preserve is None:
758            preserve = []
759        self.preserve = frozenset(preserve)
760        if noescape is None:
761            noescape = []
762        self.noescape = frozenset(noescape)
763
764    def __call__(self, stream, ctxt=None, space=XML_NAMESPACE['space'],
765                 trim_trailing_space=re.compile('[ \t]+(?=\n)').sub,
766                 collapse_lines=re.compile('\n{2,}').sub):
767        mjoin = Markup('').join
768        preserve_elems = self.preserve
769        preserve = 0
770        noescape_elems = self.noescape
771        noescape = False
772
773        textbuf = []
774        push_text = textbuf.append
775        pop_text = textbuf.pop
776        for kind, data, pos in chain(stream, [(None, None, None)]):
777
778            if kind is TEXT:
779                if noescape:
780                    data = Markup(data)
781                push_text(data)
782            else:
783                if textbuf:
784                    if len(textbuf) > 1:
785                        text = mjoin(textbuf, escape_quotes=False)
786                        del textbuf[:]
787                    else:
788                        text = escape(pop_text(), quotes=False)
789                    if not preserve:
790                        text = collapse_lines('\n', trim_trailing_space('', text))
791                    yield TEXT, Markup(text), pos
792
793                if kind is START:
794                    tag, attrs = data
795                    if preserve or (tag in preserve_elems or
796                                    attrs.get(space) == 'preserve'):
797                        preserve += 1
798                    if not noescape and tag in noescape_elems:
799                        noescape = True
800
801                elif kind is END:
802                    noescape = False
803                    if preserve:
804                        preserve -= 1
805
806                elif kind is START_CDATA:
807                    noescape = True
808
809                elif kind is END_CDATA:
810                    noescape = False
811
812                if kind:
813                    yield kind, data, pos
814
815
816class DocTypeInserter(object):
817    """A filter that inserts the DOCTYPE declaration in the correct location,
818    after the XML declaration.
819    """
820    def __init__(self, doctype):
821        """Initialize the filter.
822
823        :param doctype: DOCTYPE as a string or DocType object.
824        """
825        if isinstance(doctype, basestring):
826            doctype = DocType.get(doctype)
827        self.doctype_event = (DOCTYPE, doctype, (None, -1, -1))
828
829    def __call__(self, stream):
830        doctype_inserted = False
831        for kind, data, pos in stream:
832            if not doctype_inserted:
833                doctype_inserted = True
834                if kind is XML_DECL:
835                    yield (kind, data, pos)
836                    yield self.doctype_event
837                    continue
838                yield self.doctype_event
839
840            yield (kind, data, pos)
841
842        if not doctype_inserted:
843            yield self.doctype_event
Note: See TracBrowser for help on using the repository browser.