Edgewall Software

source: trunk/genshi/output.py @ 1164

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

Don't cache (TEXT, Markup) events in serializers. This is not needed and since Markup instances compare equal to the same non-Markup string this can lead to incorrect cached output being retrieved. Fixes #429. This is patch t429-fix.2.patch from that ticket. It includes an additional unrelated test to check that the WhitespaceFilter? actually removes ignorable whitespace.

  • Property svn:eol-style set to native
File size: 30.5 KB
RevLine 
[2]1# -*- coding: utf-8 -*-
2#
[1077]3# Copyright (C) 2006-2009 Edgewall Software
[2]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
[287]8# are also available at http://genshi.edgewall.org/wiki/License.
[2]9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
[287]12# history and logs, available at http://genshi.edgewall.org/log/.
[2]13
14"""This module provides different kinds of serialization methods for XML event
15streams.
16"""
17
[154]18from itertools import chain
19import re
[2]20
[502]21from genshi.core import escape, Attrs, Markup, Namespace, QName, StreamEventKind
[558]22from genshi.core import START, END, TEXT, XML_DECL, DOCTYPE, START_NS, END_NS, \
[494]23                        START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE
[2]24
[560]25__all__ = ['encode', 'get_serializer', 'DocType', 'XMLSerializer',
26           'XHTMLSerializer', 'HTMLSerializer', 'TextSerializer']
[517]27__docformat__ = 'restructuredtext en'
[2]28
[1086]29
[1157]30def encode(iterator, method='xml', encoding=None, out=None):
[560]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
[804]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   
[560]46    :since: version 0.4.1
[804]47    :note: Changed in 0.5: added the `out` parameter
[560]48    """
49    if encoding is not None:
50        errors = 'replace'
51        if method != 'text' and not isinstance(method, TextSerializer):
52            errors = 'xmlcharrefreplace'
[804]53        _encode = lambda string: string.encode(encoding, errors)
54    else:
55        _encode = lambda string: string
56    if out is None:
[1075]57        return _encode(''.join(list(iterator)))
[804]58    for chunk in iterator:
59        out.write(_encode(chunk))
[2]60
[1086]61
[560]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
[1163]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
[90]106class DocType(object):
107    """Defines a number of commonly used DOCTYPE declarations as constants."""
108
[502]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    )
[562]117    HTML_FRAMESET = (
118        'html', '-//W3C//DTD HTML 4.01 Frameset//EN',
119        'http://www.w3.org/TR/html4/frameset.dtd'
120    )
[90]121    HTML = HTML_STRICT
122
[562]123    HTML5 = ('html', None, None)
124
[502]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    )
[562]133    XHTML_FRAMESET = (
134        'html', '-//W3C//DTD XHTML 1.0 Frameset//EN',
135        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd'
136    )
[90]137    XHTML = XHTML_STRICT
138
[853]139    XHTML11 = (
140        'html', '-//W3C//DTD XHTML 1.1//EN',
141        'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
142    )
143
[779]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
[1031]158    @classmethod
[562]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
[869]166         * "html-frameset" for the HTML 4.01 frameset DTD
[562]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
[853]171         * "xhtml11" for the XHTML 1.1 DTD
[779]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
[562]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,
[853]189            'xhtml11': cls.XHTML11,
[779]190            'svg': cls.SVG, 'svg-full': cls.SVG_FULL,
191            'svg-basic': cls.SVG_BASIC,
192            'svg-tiny': cls.SVG_TINY
[562]193        }.get(name.lower())
[90]194
[540]195
[154]196class XMLSerializer(object):
[2]197    """Produces XML text from an event stream.
198   
[287]199    >>> from genshi.builder import tag
[21]200    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
[1076]201    >>> print(''.join(XMLSerializer()(elem.generate())))
[2]202    <div><a href="foo"/><br/><hr noshade="True"/></div>
203    """
[154]204
205    _PRESERVE_SPACE = frozenset()
206
[502]207    def __init__(self, doctype=None, strip_whitespace=True,
[1038]208                 namespace_prefixes=None, cache=True):
[90]209        """Initialize the XML serializer.
210       
[517]211        :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
212                        DOCTYPE declaration that should be included at the top
[596]213                        of the generated output, or the name of a DOCTYPE as
214                        defined in `DocType.get`
[517]215        :param strip_whitespace: whether extraneous whitespace should be
216                                 stripped from the output
[1038]217        :param cache: whether to cache the text output per event, which
218                      improves performance for repetitive markup
[596]219        :note: Changed in 0.4.2: The  `doctype` parameter can now be a string.
[1038]220        :note: Changed in 0.6: The `cache` parameter was added
[90]221        """
[263]222        self.filters = [EmptyTagFilter()]
[154]223        if strip_whitespace:
224            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
[1038]225        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
226                                               cache=cache))
[787]227        if doctype:
228            self.filters.append(DocTypeInserter(doctype))
[1038]229        self.cache = cache
[2]230
[1163]231    def _prepare_cache(self):
232        return _prepare_cache(self.cache)[:2]
233
[154]234    def __call__(self, stream):
[558]235        have_decl = have_doctype = False
[184]236        in_cdata = False
[1163]237        _emit, _get = self._prepare_cache()
[2]238
[154]239        for filter_ in self.filters:
240            stream = filter_(stream)
[2]241        for kind, data, pos in stream:
[1164]242            if kind is TEXT and isinstance(data, Markup):
243                yield data
244                continue
[1163]245            cached = _get((kind, data))
[1038]246            if cached is not None:
247                yield cached
248            elif kind is START or kind is EMPTY:
[2]249                tag, attrib = data
[502]250                buf = ['<', tag]
[485]251                for attr, value in attrib:
[502]252                    buf += [' ', attr, '="', escape(value), '"']
[485]253                buf.append(kind is EMPTY and '/>' or '>')
[1075]254                yield _emit(kind, data, Markup(''.join(buf)))
[2]255
[74]256            elif kind is END:
[1038]257                yield _emit(kind, data, Markup('</%s>' % data))
[2]258
[74]259            elif kind is TEXT:
[184]260                if in_cdata:
[1038]261                    yield _emit(kind, data, data)
[184]262                else:
[1038]263                    yield _emit(kind, data, escape(data, quotes=False))
[2]264
[94]265            elif kind is COMMENT:
[1038]266                yield _emit(kind, data, Markup('<!--%s-->' % data))
[2]267
[558]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')
[1075]277                yield Markup(''.join(buf))
[558]278                have_decl = True
279
[174]280            elif kind is DOCTYPE and not have_doctype:
281                name, pubid, sysid = data
282                buf = ['<!DOCTYPE %s']
283                if pubid:
[485]284                    buf.append(' PUBLIC "%s"')
[174]285                elif sysid:
[485]286                    buf.append(' SYSTEM')
[174]287                if sysid:
[485]288                    buf.append(' "%s"')
289                buf.append('>\n')
[1077]290                yield Markup(''.join(buf)) % tuple([p for p in data if p])
[174]291                have_doctype = True
[126]292
[184]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
[122]301            elif kind is PI:
[1038]302                yield _emit(kind, data, Markup('<?%s %s?>' % data))
[94]303
[122]304
[101]305class XHTMLSerializer(XMLSerializer):
306    """Produces XHTML text from an event stream.
[2]307   
[287]308    >>> from genshi.builder import tag
[21]309    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
[1076]310    >>> print(''.join(XHTMLSerializer()(elem.generate())))
[101]311    <div><a href="foo"></a><br /><hr noshade="noshade" /></div>
[2]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'])
[425]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    ])
[2]324
[502]325    def __init__(self, doctype=None, strip_whitespace=True,
[1038]326                 namespace_prefixes=None, drop_xml_decl=True, cache=True):
[502]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'] = ''
[1038]333        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
334                                               cache=cache))
[787]335        if doctype:
336            self.filters.append(DocTypeInserter(doctype))
[853]337        self.drop_xml_decl = drop_xml_decl
[1038]338        self.cache = cache
[502]339
[154]340    def __call__(self, stream):
[174]341        boolean_attrs = self._BOOLEAN_ATTRS
342        empty_elems = self._EMPTY_ELEMS
[853]343        drop_xml_decl = self.drop_xml_decl
344        have_decl = have_doctype = False
[184]345        in_cdata = False
[1163]346        _emit, _get = self._prepare_cache()
[90]347
[154]348        for filter_ in self.filters:
349            stream = filter_(stream)
[101]350        for kind, data, pos in stream:
[1164]351            if kind is TEXT and isinstance(data, Markup):
352                yield data
353                continue
[1163]354            cached = _get((kind, data))
[1038]355            if cached is not None:
356                yield cached
[101]357
[1038]358            elif kind is START or kind is EMPTY:
[101]359                tag, attrib = data
[502]360                buf = ['<', tag]
[485]361                for attr, value in attrib:
[502]362                    if attr in boolean_attrs:
363                        value = attr
[1075]364                    elif attr == 'xml:lang' and 'lang' not in attrib:
[628]365                        buf += [' lang="', escape(value), '"']
[1075]366                    elif attr == 'xml:space':
[805]367                        continue
[502]368                    buf += [' ', attr, '="', escape(value), '"']
[263]369                if kind is EMPTY:
[502]370                    if tag in empty_elems:
[485]371                        buf.append(' />')
[181]372                    else:
[502]373                        buf.append('></%s>' % tag)
[181]374                else:
[485]375                    buf.append('>')
[1075]376                yield _emit(kind, data, Markup(''.join(buf)))
[101]377
378            elif kind is END:
[1038]379                yield _emit(kind, data, Markup('</%s>' % data))
[101]380
381            elif kind is TEXT:
[184]382                if in_cdata:
[1038]383                    yield _emit(kind, data, data)
[184]384                else:
[1038]385                    yield _emit(kind, data, escape(data, quotes=False))
[101]386
387            elif kind is COMMENT:
[1038]388                yield _emit(kind, data, Markup('<!--%s-->' % data))
[101]389
[174]390            elif kind is DOCTYPE and not have_doctype:
391                name, pubid, sysid = data
392                buf = ['<!DOCTYPE %s']
393                if pubid:
[485]394                    buf.append(' PUBLIC "%s"')
[174]395                elif sysid:
[485]396                    buf.append(' SYSTEM')
[174]397                if sysid:
[485]398                    buf.append(' "%s"')
399                buf.append('>\n')
[1077]400                yield Markup(''.join(buf)) % tuple([p for p in data if p])
[174]401                have_doctype = True
[126]402
[853]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')
[1075]412                yield Markup(''.join(buf))
[853]413                have_decl = True
414
[184]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
[122]423            elif kind is PI:
[1038]424                yield _emit(kind, data, Markup('<?%s %s?>' % data))
[101]425
[122]426
[101]427class HTMLSerializer(XHTMLSerializer):
428    """Produces HTML text from an event stream.
429   
[287]430    >>> from genshi.builder import tag
[101]431    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
[1076]432    >>> print(''.join(HTMLSerializer()(elem.generate())))
[101]433    <div><a href="foo"></a><br><hr noshade></div>
434    """
435
[502]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    ])
[181]440
[1038]441    def __init__(self, doctype=None, strip_whitespace=True, cache=True):
[181]442        """Initialize the HTML serializer.
443       
[517]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
[1038]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
[181]452        """
453        super(HTMLSerializer, self).__init__(doctype, False)
[502]454        self.filters = [EmptyTagFilter()]
[181]455        if strip_whitespace:
456            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
[378]457                                                 self._NOESCAPE_ELEMS))
[628]458        self.filters.append(NamespaceFlattener(prefixes={
459            'http://www.w3.org/1999/xhtml': ''
[1038]460        }, cache=cache))
[787]461        if doctype:
462            self.filters.append(DocTypeInserter(doctype))
[1038]463        self.cache = True
[181]464
[154]465    def __call__(self, stream):
[174]466        boolean_attrs = self._BOOLEAN_ATTRS
467        empty_elems = self._EMPTY_ELEMS
[181]468        noescape_elems = self._NOESCAPE_ELEMS
[90]469        have_doctype = False
[181]470        noescape = False
[1163]471        _emit, _get = self._prepare_cache()
[2]472
[154]473        for filter_ in self.filters:
474            stream = filter_(stream)
[1038]475        for kind, data, _ in stream:
[1164]476            if kind is TEXT and isinstance(data, Markup):
477                yield data
478                continue
[1163]479            output = _get((kind, data))
[1038]480            if output is not None:
481                yield output
[1040]482                if (kind is START or kind is EMPTY) \
483                        and data[0] in noescape_elems:
[1038]484                    noescape = True
485                elif kind is END:
486                    noescape = False
[2]487
[1038]488            elif kind is START or kind is EMPTY:
[2]489                tag, attrib = data
[502]490                buf = ['<', tag]
491                for attr, value in attrib:
492                    if attr in boolean_attrs:
493                        if value:
494                            buf += [' ', attr]
[628]495                    elif ':' in attr:
[1075]496                        if attr == 'xml:lang' and 'lang' not in attrib:
[628]497                            buf += [' lang="', escape(value), '"']
498                    elif attr != 'xmlns':
[502]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)
[1075]504                yield _emit(kind, data, Markup(''.join(buf)))
[502]505                if tag in noescape_elems:
506                    noescape = True
[101]507
[74]508            elif kind is END:
[1038]509                yield _emit(kind, data, Markup('</%s>' % data))
[181]510                noescape = False
511
[74]512            elif kind is TEXT:
[181]513                if noescape:
[1038]514                    yield _emit(kind, data, data)
[181]515                else:
[1038]516                    yield _emit(kind, data, escape(data, quotes=False))
[2]517
[94]518            elif kind is COMMENT:
[1038]519                yield _emit(kind, data, Markup('<!--%s-->' % data))
[2]520
[174]521            elif kind is DOCTYPE and not have_doctype:
522                name, pubid, sysid = data
523                buf = ['<!DOCTYPE %s']
524                if pubid:
[485]525                    buf.append(' PUBLIC "%s"')
[174]526                elif sysid:
[485]527                    buf.append(' SYSTEM')
[174]528                if sysid:
[485]529                    buf.append(' "%s"')
530                buf.append('>\n')
[1077]531                yield Markup(''.join(buf)) % tuple([p for p in data if p])
[174]532                have_doctype = True
[126]533
[122]534            elif kind is PI:
[1038]535                yield _emit(kind, data, Markup('<?%s %s?>' % data))
[94]536
[122]537
[250]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   
[287]544    >>> from genshi.builder import tag
[250]545    >>> elem = tag.div(tag.a('<Hello!>', href='foo'), tag.br)
[1076]546    >>> print(elem)
[250]547    <div><a href="foo">&lt;Hello!&gt;</a><br/></div>
[1076]548    >>> print(''.join(TextSerializer()(elem.generate())))
[250]549    <Hello!>
550
551    If text events contain literal markup (instances of the `Markup` class),
[774]552    that markup is by default passed through unchanged:
[250]553   
[774]554    >>> elem = tag.div(Markup('<a href="foo">Hello &amp; Bye!</a><br/>'))
[1086]555    >>> print(elem.generate().render(TextSerializer, encoding=None))
[774]556    <a href="foo">Hello &amp; Bye!</a><br/>
557   
[864]558    You can use the ``strip_markup`` to change this behavior, so that tags and
[774]559    entities are stripped from the output (or in the case of entities,
560    replaced with the equivalent character):
561
[1086]562    >>> print(elem.generate().render(TextSerializer, strip_markup=True,
563    ...                              encoding=None))
[774]564    Hello & Bye!
[250]565    """
566
[774]567    def __init__(self, strip_markup=False):
[864]568        """Create the serializer.
569       
570        :param strip_markup: whether markup (tags and encoded characters) found
571                             in the text should be removed
572        """
[774]573        self.strip_markup = strip_markup
574
[250]575    def __call__(self, stream):
[774]576        strip_markup = self.strip_markup
[502]577        for event in stream:
578            if event[0] is TEXT:
579                data = event[1]
[774]580                if strip_markup and type(data) is Markup:
[250]581                    data = data.striptags().stripentities()
[251]582                yield unicode(data)
[250]583
584
[263]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)
[502]594        for ev in stream:
[263]595            if prev[0] is START:
[502]596                if ev[0] is END:
[263]597                    prev = EMPTY, prev[1], prev[2]
598                    yield prev
599                    continue
600                else:
601                    yield prev
[502]602            if ev[0] is not START:
603                yield ev
604            prev = ev
[263]605
606
607EMPTY = EmptyTagFilter.EMPTY
608
609
[502]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   
[517]614    :param prefixes: optional mapping of namespace URIs to prefixes
[502]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):
[1076]621    ...     print('%s %r' % (kind, data))
[1075]622    START (u'doc', Attrs([('xmlns', u'NS1'), (u'xmlns:two', u'NS2')]))
[502]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
[1038]630    def __init__(self, prefixes=None, cache=True):
[502]631        self.prefixes = {XML_NAMESPACE.uri: 'xml'}
632        if prefixes is not None:
633            self.prefixes.update(prefixes)
[1038]634        self.cache = cache
[502]635
636    def __call__(self, stream):
637        prefixes = dict([(v, [k]) for k, v in self.prefixes.items()])
638        namespaces = {XML_NAMESPACE.uri: ['xml']}
[1163]639        _emit, _get, cache = _prepare_cache(self.cache)
[502]640        def _push_ns(prefix, uri):
641            namespaces.setdefault(uri, []).append(prefix)
642            prefixes.setdefault(prefix, []).append(uri)
[1038]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
[502]656
657        ns_attrs = []
658        _push_ns_attr = ns_attrs.append
[529]659        def _make_ns_attr(prefix, uri):
[1075]660            return 'xmlns%s' % (prefix and ':%s' % prefix or ''), uri
[502]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:
[1164]670            if kind is TEXT and isinstance(data, Markup):
671                yield kind, data, pos
672                continue
[1163]673            output = _get((kind, data))
[1038]674            if output is not None:
675                yield kind, output, pos
[502]676
[1038]677            elif kind is START or kind is EMPTY:
[502]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:
[1075]686                            tagname = '%s:%s' % (prefix, tagname)
[502]687                    else:
[1075]688                        _push_ns_attr(('xmlns', tagns))
[502]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)
[504]699                            _push_ns_attr(('xmlns:%s' % prefix, attrns))
[502]700                        else:
701                            prefix = namespaces[attrns][-1]
702                        if prefix:
[1075]703                            attrname = '%s:%s' % (prefix, attrname)
[502]704                    new_attrs.append((attrname, value))
705
[1163]706                data = _emit(kind, data, (tagname, Attrs(ns_attrs + new_attrs)))
707                yield kind, data, pos
[502]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:
[1075]716                        tagname = '%s:%s' % (prefix, tagname)
[1163]717                yield kind, _emit(kind, data, tagname), pos
[502]718
719            elif kind is START_NS:
720                prefix, uri = data
721                if uri not in namespaces:
722                    prefix = prefixes.get(uri, [prefix])[-1]
[529]723                    _push_ns_attr(_make_ns_attr(prefix, uri))
[502]724                _push_ns(prefix, uri)
725
726            elif kind is END_NS:
727                if data in prefixes:
[1038]728                    uri = _pop_ns(data)
[529]729                    if ns_attrs:
730                        attr = _make_ns_attr(data, uri)
731                        if attr in ns_attrs:
732                            ns_attrs.remove(attr)
[502]733
734            else:
735                yield kind, data, pos
736
737
[154]738class WhitespaceFilter(object):
739    """A filter that removes extraneous ignorable white space from the
[502]740    stream.
741    """
[154]742
[378]743    def __init__(self, preserve=None, noescape=None):
[154]744        """Initialize the filter.
745       
[517]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
[181]750       
[425]751        The `noescape` set is expected to refer to elements that cannot contain
[517]752        further child elements (such as ``<style>`` or ``<script>`` in HTML
753        documents).
[154]754        """
755        if preserve is None:
756            preserve = []
757        self.preserve = frozenset(preserve)
[181]758        if noescape is None:
759            noescape = []
760        self.noescape = frozenset(noescape)
[154]761
[275]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):
[154]765        mjoin = Markup('').join
[181]766        preserve_elems = self.preserve
[425]767        preserve = 0
[181]768        noescape_elems = self.noescape
769        noescape = False
[154]770
771        textbuf = []
[181]772        push_text = textbuf.append
[174]773        pop_text = textbuf.pop
[154]774        for kind, data, pos in chain(stream, [(None, None, None)]):
[502]775
[154]776            if kind is TEXT:
[181]777                if noescape:
778                    data = Markup(data)
779                push_text(data)
[154]780            else:
781                if textbuf:
782                    if len(textbuf) > 1:
783                        text = mjoin(textbuf, escape_quotes=False)
784                        del textbuf[:]
785                    else:
[174]786                        text = escape(pop_text(), quotes=False)
[181]787                    if not preserve:
[154]788                        text = collapse_lines('\n', trim_trailing_space('', text))
789                    yield TEXT, Markup(text), pos
[181]790
791                if kind is START:
[425]792                    tag, attrs = data
793                    if preserve or (tag in preserve_elems or
794                                    attrs.get(space) == 'preserve'):
795                        preserve += 1
[275]796                    if not noescape and tag in noescape_elems:
[181]797                        noescape = True
798
799                elif kind is END:
[425]800                    noescape = False
801                    if preserve:
802                        preserve -= 1
[181]803
[378]804                elif kind is START_CDATA:
[184]805                    noescape = True
806
[378]807                elif kind is END_CDATA:
[184]808                    noescape = False
809
[174]810                if kind:
[154]811                    yield kind, data, pos
[787]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:
[788]831                doctype_inserted = True
832                if kind is XML_DECL:
833                    yield (kind, data, pos)
834                    yield self.doctype_event
[787]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.