Edgewall Software

source: trunk/genshi/output.py

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

Add missing boolean attributes to XHTML and HTML serializers (fixes #570).

  • Property svn:eol-style set to native
File size: 30.7 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',
[1260]319                                'nohref', 'noresize', 'noshade', 'nowrap',
320                                'autofocus', 'readonly', 'required',
321                                'formnovalidate'])
[425]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    ])
[2]326
[502]327    def __init__(self, doctype=None, strip_whitespace=True,
[1038]328                 namespace_prefixes=None, drop_xml_decl=True, cache=True):
[502]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'] = ''
[1038]335        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
336                                               cache=cache))
[787]337        if doctype:
338            self.filters.append(DocTypeInserter(doctype))
[853]339        self.drop_xml_decl = drop_xml_decl
[1038]340        self.cache = cache
[502]341
[154]342    def __call__(self, stream):
[174]343        boolean_attrs = self._BOOLEAN_ATTRS
344        empty_elems = self._EMPTY_ELEMS
[853]345        drop_xml_decl = self.drop_xml_decl
346        have_decl = have_doctype = False
[184]347        in_cdata = False
[1163]348        _emit, _get = self._prepare_cache()
[90]349
[154]350        for filter_ in self.filters:
351            stream = filter_(stream)
[101]352        for kind, data, pos in stream:
[1164]353            if kind is TEXT and isinstance(data, Markup):
354                yield data
355                continue
[1163]356            cached = _get((kind, data))
[1038]357            if cached is not None:
358                yield cached
[101]359
[1038]360            elif kind is START or kind is EMPTY:
[101]361                tag, attrib = data
[502]362                buf = ['<', tag]
[485]363                for attr, value in attrib:
[502]364                    if attr in boolean_attrs:
365                        value = attr
[1075]366                    elif attr == 'xml:lang' and 'lang' not in attrib:
[628]367                        buf += [' lang="', escape(value), '"']
[1075]368                    elif attr == 'xml:space':
[805]369                        continue
[502]370                    buf += [' ', attr, '="', escape(value), '"']
[263]371                if kind is EMPTY:
[502]372                    if tag in empty_elems:
[485]373                        buf.append(' />')
[181]374                    else:
[502]375                        buf.append('></%s>' % tag)
[181]376                else:
[485]377                    buf.append('>')
[1075]378                yield _emit(kind, data, Markup(''.join(buf)))
[101]379
380            elif kind is END:
[1038]381                yield _emit(kind, data, Markup('</%s>' % data))
[101]382
383            elif kind is TEXT:
[184]384                if in_cdata:
[1038]385                    yield _emit(kind, data, data)
[184]386                else:
[1038]387                    yield _emit(kind, data, escape(data, quotes=False))
[101]388
389            elif kind is COMMENT:
[1038]390                yield _emit(kind, data, Markup('<!--%s-->' % data))
[101]391
[174]392            elif kind is DOCTYPE and not have_doctype:
393                name, pubid, sysid = data
394                buf = ['<!DOCTYPE %s']
395                if pubid:
[485]396                    buf.append(' PUBLIC "%s"')
[174]397                elif sysid:
[485]398                    buf.append(' SYSTEM')
[174]399                if sysid:
[485]400                    buf.append(' "%s"')
401                buf.append('>\n')
[1077]402                yield Markup(''.join(buf)) % tuple([p for p in data if p])
[174]403                have_doctype = True
[126]404
[853]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')
[1075]414                yield Markup(''.join(buf))
[853]415                have_decl = True
416
[184]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
[122]425            elif kind is PI:
[1038]426                yield _emit(kind, data, Markup('<?%s %s?>' % data))
[101]427
[122]428
[101]429class HTMLSerializer(XHTMLSerializer):
430    """Produces HTML text from an event stream.
431   
[287]432    >>> from genshi.builder import tag
[101]433    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
[1076]434    >>> print(''.join(HTMLSerializer()(elem.generate())))
[101]435    <div><a href="foo"></a><br><hr noshade></div>
436    """
437
[502]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    ])
[181]442
[1038]443    def __init__(self, doctype=None, strip_whitespace=True, cache=True):
[181]444        """Initialize the HTML serializer.
445       
[517]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
[1038]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
[181]454        """
455        super(HTMLSerializer, self).__init__(doctype, False)
[502]456        self.filters = [EmptyTagFilter()]
[181]457        if strip_whitespace:
458            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
[378]459                                                 self._NOESCAPE_ELEMS))
[628]460        self.filters.append(NamespaceFlattener(prefixes={
461            'http://www.w3.org/1999/xhtml': ''
[1038]462        }, cache=cache))
[787]463        if doctype:
464            self.filters.append(DocTypeInserter(doctype))
[1038]465        self.cache = True
[181]466
[154]467    def __call__(self, stream):
[174]468        boolean_attrs = self._BOOLEAN_ATTRS
469        empty_elems = self._EMPTY_ELEMS
[181]470        noescape_elems = self._NOESCAPE_ELEMS
[90]471        have_doctype = False
[181]472        noescape = False
[1163]473        _emit, _get = self._prepare_cache()
[2]474
[154]475        for filter_ in self.filters:
476            stream = filter_(stream)
[1038]477        for kind, data, _ in stream:
[1164]478            if kind is TEXT and isinstance(data, Markup):
479                yield data
480                continue
[1163]481            output = _get((kind, data))
[1038]482            if output is not None:
483                yield output
[1040]484                if (kind is START or kind is EMPTY) \
485                        and data[0] in noescape_elems:
[1038]486                    noescape = True
487                elif kind is END:
488                    noescape = False
[2]489
[1038]490            elif kind is START or kind is EMPTY:
[2]491                tag, attrib = data
[502]492                buf = ['<', tag]
493                for attr, value in attrib:
494                    if attr in boolean_attrs:
495                        if value:
496                            buf += [' ', attr]
[628]497                    elif ':' in attr:
[1075]498                        if attr == 'xml:lang' and 'lang' not in attrib:
[628]499                            buf += [' lang="', escape(value), '"']
500                    elif attr != 'xmlns':
[502]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)
[1075]506                yield _emit(kind, data, Markup(''.join(buf)))
[502]507                if tag in noescape_elems:
508                    noescape = True
[101]509
[74]510            elif kind is END:
[1038]511                yield _emit(kind, data, Markup('</%s>' % data))
[181]512                noescape = False
513
[74]514            elif kind is TEXT:
[181]515                if noescape:
[1038]516                    yield _emit(kind, data, data)
[181]517                else:
[1038]518                    yield _emit(kind, data, escape(data, quotes=False))
[2]519
[94]520            elif kind is COMMENT:
[1038]521                yield _emit(kind, data, Markup('<!--%s-->' % data))
[2]522
[174]523            elif kind is DOCTYPE and not have_doctype:
524                name, pubid, sysid = data
525                buf = ['<!DOCTYPE %s']
526                if pubid:
[485]527                    buf.append(' PUBLIC "%s"')
[174]528                elif sysid:
[485]529                    buf.append(' SYSTEM')
[174]530                if sysid:
[485]531                    buf.append(' "%s"')
532                buf.append('>\n')
[1077]533                yield Markup(''.join(buf)) % tuple([p for p in data if p])
[174]534                have_doctype = True
[126]535
[122]536            elif kind is PI:
[1038]537                yield _emit(kind, data, Markup('<?%s %s?>' % data))
[94]538
[122]539
[250]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   
[287]546    >>> from genshi.builder import tag
[250]547    >>> elem = tag.div(tag.a('<Hello!>', href='foo'), tag.br)
[1076]548    >>> print(elem)
[250]549    <div><a href="foo">&lt;Hello!&gt;</a><br/></div>
[1076]550    >>> print(''.join(TextSerializer()(elem.generate())))
[250]551    <Hello!>
552
553    If text events contain literal markup (instances of the `Markup` class),
[774]554    that markup is by default passed through unchanged:
[250]555   
[774]556    >>> elem = tag.div(Markup('<a href="foo">Hello &amp; Bye!</a><br/>'))
[1086]557    >>> print(elem.generate().render(TextSerializer, encoding=None))
[774]558    <a href="foo">Hello &amp; Bye!</a><br/>
559   
[864]560    You can use the ``strip_markup`` to change this behavior, so that tags and
[774]561    entities are stripped from the output (or in the case of entities,
562    replaced with the equivalent character):
563
[1086]564    >>> print(elem.generate().render(TextSerializer, strip_markup=True,
565    ...                              encoding=None))
[774]566    Hello & Bye!
[250]567    """
568
[774]569    def __init__(self, strip_markup=False):
[864]570        """Create the serializer.
571       
572        :param strip_markup: whether markup (tags and encoded characters) found
573                             in the text should be removed
574        """
[774]575        self.strip_markup = strip_markup
576
[250]577    def __call__(self, stream):
[774]578        strip_markup = self.strip_markup
[502]579        for event in stream:
580            if event[0] is TEXT:
581                data = event[1]
[774]582                if strip_markup and type(data) is Markup:
[250]583                    data = data.striptags().stripentities()
[251]584                yield unicode(data)
[250]585
586
[263]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)
[502]596        for ev in stream:
[263]597            if prev[0] is START:
[502]598                if ev[0] is END:
[263]599                    prev = EMPTY, prev[1], prev[2]
600                    yield prev
601                    continue
602                else:
603                    yield prev
[502]604            if ev[0] is not START:
605                yield ev
606            prev = ev
[263]607
608
609EMPTY = EmptyTagFilter.EMPTY
610
611
[502]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   
[517]616    :param prefixes: optional mapping of namespace URIs to prefixes
[502]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):
[1076]623    ...     print('%s %r' % (kind, data))
[1075]624    START (u'doc', Attrs([('xmlns', u'NS1'), (u'xmlns:two', u'NS2')]))
[502]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
[1038]632    def __init__(self, prefixes=None, cache=True):
[502]633        self.prefixes = {XML_NAMESPACE.uri: 'xml'}
634        if prefixes is not None:
635            self.prefixes.update(prefixes)
[1038]636        self.cache = cache
[502]637
638    def __call__(self, stream):
639        prefixes = dict([(v, [k]) for k, v in self.prefixes.items()])
640        namespaces = {XML_NAMESPACE.uri: ['xml']}
[1163]641        _emit, _get, cache = _prepare_cache(self.cache)
[502]642        def _push_ns(prefix, uri):
643            namespaces.setdefault(uri, []).append(prefix)
644            prefixes.setdefault(prefix, []).append(uri)
[1038]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
[502]658
659        ns_attrs = []
660        _push_ns_attr = ns_attrs.append
[529]661        def _make_ns_attr(prefix, uri):
[1075]662            return 'xmlns%s' % (prefix and ':%s' % prefix or ''), uri
[502]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:
[1164]672            if kind is TEXT and isinstance(data, Markup):
673                yield kind, data, pos
674                continue
[1163]675            output = _get((kind, data))
[1038]676            if output is not None:
677                yield kind, output, pos
[502]678
[1038]679            elif kind is START or kind is EMPTY:
[502]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:
[1075]688                            tagname = '%s:%s' % (prefix, tagname)
[502]689                    else:
[1075]690                        _push_ns_attr(('xmlns', tagns))
[502]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)
[504]701                            _push_ns_attr(('xmlns:%s' % prefix, attrns))
[502]702                        else:
703                            prefix = namespaces[attrns][-1]
704                        if prefix:
[1075]705                            attrname = '%s:%s' % (prefix, attrname)
[502]706                    new_attrs.append((attrname, value))
707
[1163]708                data = _emit(kind, data, (tagname, Attrs(ns_attrs + new_attrs)))
709                yield kind, data, pos
[502]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:
[1075]718                        tagname = '%s:%s' % (prefix, tagname)
[1163]719                yield kind, _emit(kind, data, tagname), pos
[502]720
721            elif kind is START_NS:
722                prefix, uri = data
723                if uri not in namespaces:
724                    prefix = prefixes.get(uri, [prefix])[-1]
[529]725                    _push_ns_attr(_make_ns_attr(prefix, uri))
[502]726                _push_ns(prefix, uri)
727
728            elif kind is END_NS:
729                if data in prefixes:
[1038]730                    uri = _pop_ns(data)
[529]731                    if ns_attrs:
732                        attr = _make_ns_attr(data, uri)
733                        if attr in ns_attrs:
734                            ns_attrs.remove(attr)
[502]735
736            else:
737                yield kind, data, pos
738
739
[154]740class WhitespaceFilter(object):
741    """A filter that removes extraneous ignorable white space from the
[502]742    stream.
743    """
[154]744
[378]745    def __init__(self, preserve=None, noescape=None):
[154]746        """Initialize the filter.
747       
[517]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
[181]752       
[425]753        The `noescape` set is expected to refer to elements that cannot contain
[517]754        further child elements (such as ``<style>`` or ``<script>`` in HTML
755        documents).
[154]756        """
757        if preserve is None:
758            preserve = []
759        self.preserve = frozenset(preserve)
[181]760        if noescape is None:
761            noescape = []
762        self.noescape = frozenset(noescape)
[154]763
[275]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):
[154]767        mjoin = Markup('').join
[181]768        preserve_elems = self.preserve
[425]769        preserve = 0
[181]770        noescape_elems = self.noescape
771        noescape = False
[154]772
773        textbuf = []
[181]774        push_text = textbuf.append
[174]775        pop_text = textbuf.pop
[154]776        for kind, data, pos in chain(stream, [(None, None, None)]):
[502]777
[154]778            if kind is TEXT:
[181]779                if noescape:
780                    data = Markup(data)
781                push_text(data)
[154]782            else:
783                if textbuf:
784                    if len(textbuf) > 1:
785                        text = mjoin(textbuf, escape_quotes=False)
786                        del textbuf[:]
787                    else:
[174]788                        text = escape(pop_text(), quotes=False)
[181]789                    if not preserve:
[154]790                        text = collapse_lines('\n', trim_trailing_space('', text))
791                    yield TEXT, Markup(text), pos
[181]792
793                if kind is START:
[425]794                    tag, attrs = data
795                    if preserve or (tag in preserve_elems or
796                                    attrs.get(space) == 'preserve'):
797                        preserve += 1
[275]798                    if not noescape and tag in noescape_elems:
[181]799                        noescape = True
800
801                elif kind is END:
[425]802                    noescape = False
803                    if preserve:
804                        preserve -= 1
[181]805
[378]806                elif kind is START_CDATA:
[184]807                    noescape = True
808
[378]809                elif kind is END_CDATA:
[184]810                    noescape = False
811
[174]812                if kind:
[154]813                    yield kind, data, pos
[787]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:
[788]833                doctype_inserted = True
834                if kind is XML_DECL:
835                    yield (kind, data, pos)
836                    yield self.doctype_event
[787]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.