Edgewall Software

source: tags/0.4.1/genshi/output.py

Last change on this file was 563, checked in by cmlenz, 16 years ago

Ported [562] to 0.4.x.

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