Edgewall Software

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

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

Ported [594:596] to 0.4.x branch.

  • Property svn:eol-style set to native
File size: 25.1 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, or the name of a DOCTYPE as
154                        defined in `DocType.get`
155        :param strip_whitespace: whether extraneous whitespace should be
156                                 stripped from the output
157        :note: Changed in 0.4.2: The  `doctype` parameter can now be a string.
158        """
159        self.preamble = []
160        if doctype:
161            if isinstance(doctype, basestring):
162                doctype = DocType.get(doctype)
163            self.preamble.append((DOCTYPE, doctype, (None, -1, -1)))
164        self.filters = [EmptyTagFilter()]
165        if strip_whitespace:
166            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
167        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
168
169    def __call__(self, stream):
170        have_decl = have_doctype = False
171        in_cdata = False
172
173        stream = chain(self.preamble, stream)
174        for filter_ in self.filters:
175            stream = filter_(stream)
176        for kind, data, pos in stream:
177
178            if kind is START or kind is EMPTY:
179                tag, attrib = data
180                buf = ['<', tag]
181                for attr, value in attrib:
182                    buf += [' ', attr, '="', escape(value), '"']
183                buf.append(kind is EMPTY and '/>' or '>')
184                yield Markup(u''.join(buf))
185
186            elif kind is END:
187                yield Markup('</%s>' % data)
188
189            elif kind is TEXT:
190                if in_cdata:
191                    yield data
192                else:
193                    yield escape(data, quotes=False)
194
195            elif kind is COMMENT:
196                yield Markup('<!--%s-->' % data)
197
198            elif kind is XML_DECL and not have_decl:
199                version, encoding, standalone = data
200                buf = ['<?xml version="%s"' % version]
201                if encoding:
202                    buf.append(' encoding="%s"' % encoding)
203                if standalone != -1:
204                    standalone = standalone and 'yes' or 'no'
205                    buf.append(' standalone="%s"' % standalone)
206                buf.append('?>\n')
207                yield Markup(u''.join(buf))
208                have_decl = True
209
210            elif kind is DOCTYPE and not have_doctype:
211                name, pubid, sysid = data
212                buf = ['<!DOCTYPE %s']
213                if pubid:
214                    buf.append(' PUBLIC "%s"')
215                elif sysid:
216                    buf.append(' SYSTEM')
217                if sysid:
218                    buf.append(' "%s"')
219                buf.append('>\n')
220                yield Markup(u''.join(buf), *filter(None, data))
221                have_doctype = True
222
223            elif kind is START_CDATA:
224                yield Markup('<![CDATA[')
225                in_cdata = True
226
227            elif kind is END_CDATA:
228                yield Markup(']]>')
229                in_cdata = False
230
231            elif kind is PI:
232                yield Markup('<?%s %s?>' % data)
233
234
235class XHTMLSerializer(XMLSerializer):
236    """Produces XHTML text from an event stream.
237   
238    >>> from genshi.builder import tag
239    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
240    >>> print ''.join(XHTMLSerializer()(elem.generate()))
241    <div><a href="foo"></a><br /><hr noshade="noshade" /></div>
242    """
243
244    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
245                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
246                              'param'])
247    _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
248                                'defer', 'disabled', 'ismap', 'multiple',
249                                'nohref', 'noresize', 'noshade', 'nowrap'])
250    _PRESERVE_SPACE = frozenset([
251        QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'),
252        QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea')
253    ])
254
255    def __init__(self, doctype=None, strip_whitespace=True,
256                 namespace_prefixes=None):
257        super(XHTMLSerializer, self).__init__(doctype, False)
258        self.filters = [EmptyTagFilter()]
259        if strip_whitespace:
260            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
261        namespace_prefixes = namespace_prefixes or {}
262        namespace_prefixes['http://www.w3.org/1999/xhtml'] = ''
263        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
264
265    def __call__(self, stream):
266        boolean_attrs = self._BOOLEAN_ATTRS
267        empty_elems = self._EMPTY_ELEMS
268        have_doctype = False
269        in_cdata = False
270
271        stream = chain(self.preamble, stream)
272        for filter_ in self.filters:
273            stream = filter_(stream)
274        for kind, data, pos in stream:
275
276            if kind is START or kind is EMPTY:
277                tag, attrib = data
278                buf = ['<', tag]
279                for attr, value in attrib:
280                    if attr in boolean_attrs:
281                        value = attr
282                    buf += [' ', attr, '="', escape(value), '"']
283                if kind is EMPTY:
284                    if tag in empty_elems:
285                        buf.append(' />')
286                    else:
287                        buf.append('></%s>' % tag)
288                else:
289                    buf.append('>')
290                yield Markup(u''.join(buf))
291
292            elif kind is END:
293                yield Markup('</%s>' % data)
294
295            elif kind is TEXT:
296                if in_cdata:
297                    yield data
298                else:
299                    yield escape(data, quotes=False)
300
301            elif kind is COMMENT:
302                yield Markup('<!--%s-->' % data)
303
304            elif kind is DOCTYPE and not have_doctype:
305                name, pubid, sysid = data
306                buf = ['<!DOCTYPE %s']
307                if pubid:
308                    buf.append(' PUBLIC "%s"')
309                elif sysid:
310                    buf.append(' SYSTEM')
311                if sysid:
312                    buf.append(' "%s"')
313                buf.append('>\n')
314                yield Markup(u''.join(buf), *filter(None, data))
315                have_doctype = True
316
317            elif kind is START_CDATA:
318                yield Markup('<![CDATA[')
319                in_cdata = True
320
321            elif kind is END_CDATA:
322                yield Markup(']]>')
323                in_cdata = False
324
325            elif kind is PI:
326                yield Markup('<?%s %s?>' % data)
327
328
329class HTMLSerializer(XHTMLSerializer):
330    """Produces HTML text from an event stream.
331   
332    >>> from genshi.builder import tag
333    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
334    >>> print ''.join(HTMLSerializer()(elem.generate()))
335    <div><a href="foo"></a><br><hr noshade></div>
336    """
337
338    _NOESCAPE_ELEMS = frozenset([
339        QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
340        QName('style'), QName('http://www.w3.org/1999/xhtml}style')
341    ])
342
343    def __init__(self, doctype=None, strip_whitespace=True):
344        """Initialize the HTML serializer.
345       
346        :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
347                        DOCTYPE declaration that should be included at the top
348                        of the generated output
349        :param strip_whitespace: whether extraneous whitespace should be
350                                 stripped from the output
351        """
352        super(HTMLSerializer, self).__init__(doctype, False)
353        self.filters = [EmptyTagFilter()]
354        if strip_whitespace:
355            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
356                                                 self._NOESCAPE_ELEMS))
357        self.filters.append(NamespaceStripper('http://www.w3.org/1999/xhtml'))
358
359    def __call__(self, stream):
360        boolean_attrs = self._BOOLEAN_ATTRS
361        empty_elems = self._EMPTY_ELEMS
362        noescape_elems = self._NOESCAPE_ELEMS
363        have_doctype = False
364        noescape = False
365
366        stream = chain(self.preamble, stream)
367        for filter_ in self.filters:
368            stream = filter_(stream)
369        for kind, data, pos in stream:
370
371            if kind is START or kind is EMPTY:
372                tag, attrib = data
373                buf = ['<', tag]
374                for attr, value in attrib:
375                    if attr in boolean_attrs:
376                        if value:
377                            buf += [' ', attr]
378                    else:
379                        buf += [' ', attr, '="', escape(value), '"']
380                buf.append('>')
381                if kind is EMPTY:
382                    if tag not in empty_elems:
383                        buf.append('</%s>' % tag)
384                yield Markup(u''.join(buf))
385                if tag in noescape_elems:
386                    noescape = True
387
388            elif kind is END:
389                yield Markup('</%s>' % data)
390                noescape = False
391
392            elif kind is TEXT:
393                if noescape:
394                    yield data
395                else:
396                    yield escape(data, quotes=False)
397
398            elif kind is COMMENT:
399                yield Markup('<!--%s-->' % data)
400
401            elif kind is DOCTYPE and not have_doctype:
402                name, pubid, sysid = data
403                buf = ['<!DOCTYPE %s']
404                if pubid:
405                    buf.append(' PUBLIC "%s"')
406                elif sysid:
407                    buf.append(' SYSTEM')
408                if sysid:
409                    buf.append(' "%s"')
410                buf.append('>\n')
411                yield Markup(u''.join(buf), *filter(None, data))
412                have_doctype = True
413
414            elif kind is PI:
415                yield Markup('<?%s %s?>' % data)
416
417
418class TextSerializer(object):
419    """Produces plain text from an event stream.
420   
421    Only text events are included in the output. Unlike the other serializer,
422    special XML characters are not escaped:
423   
424    >>> from genshi.builder import tag
425    >>> elem = tag.div(tag.a('<Hello!>', href='foo'), tag.br)
426    >>> print elem
427    <div><a href="foo">&lt;Hello!&gt;</a><br/></div>
428    >>> print ''.join(TextSerializer()(elem.generate()))
429    <Hello!>
430
431    If text events contain literal markup (instances of the `Markup` class),
432    tags or entities are stripped from the output:
433   
434    >>> elem = tag.div(Markup('<a href="foo">Hello!</a><br/>'))
435    >>> print elem
436    <div><a href="foo">Hello!</a><br/></div>
437    >>> print ''.join(TextSerializer()(elem.generate()))
438    Hello!
439    """
440
441    def __call__(self, stream):
442        for event in stream:
443            if event[0] is TEXT:
444                data = event[1]
445                if type(data) is Markup:
446                    data = data.striptags().stripentities()
447                yield unicode(data)
448
449
450class EmptyTagFilter(object):
451    """Combines `START` and `STOP` events into `EMPTY` events for elements that
452    have no contents.
453    """
454
455    EMPTY = StreamEventKind('EMPTY')
456
457    def __call__(self, stream):
458        prev = (None, None, None)
459        for ev in stream:
460            if prev[0] is START:
461                if ev[0] is END:
462                    prev = EMPTY, prev[1], prev[2]
463                    yield prev
464                    continue
465                else:
466                    yield prev
467            if ev[0] is not START:
468                yield ev
469            prev = ev
470
471
472EMPTY = EmptyTagFilter.EMPTY
473
474
475class NamespaceFlattener(object):
476    r"""Output stream filter that removes namespace information from the stream,
477    instead adding namespace attributes and prefixes as needed.
478   
479    :param prefixes: optional mapping of namespace URIs to prefixes
480   
481    >>> from genshi.input import XML
482    >>> xml = XML('''<doc xmlns="NS1" xmlns:two="NS2">
483    ...   <two:item/>
484    ... </doc>''')
485    >>> for kind, data, pos in NamespaceFlattener()(xml):
486    ...     print kind, repr(data)
487    START (u'doc', Attrs([(u'xmlns', u'NS1'), (u'xmlns:two', u'NS2')]))
488    TEXT u'\n  '
489    START (u'two:item', Attrs())
490    END u'two:item'
491    TEXT u'\n'
492    END u'doc'
493    """
494
495    def __init__(self, prefixes=None):
496        self.prefixes = {XML_NAMESPACE.uri: 'xml'}
497        if prefixes is not None:
498            self.prefixes.update(prefixes)
499
500    def __call__(self, stream):
501        prefixes = dict([(v, [k]) for k, v in self.prefixes.items()])
502        namespaces = {XML_NAMESPACE.uri: ['xml']}
503        def _push_ns(prefix, uri):
504            namespaces.setdefault(uri, []).append(prefix)
505            prefixes.setdefault(prefix, []).append(uri)
506
507        ns_attrs = []
508        _push_ns_attr = ns_attrs.append
509        def _make_ns_attr(prefix, uri):
510            return u'xmlns%s' % (prefix and ':%s' % prefix or ''), uri
511
512        def _gen_prefix():
513            val = 0
514            while 1:
515                val += 1
516                yield 'ns%d' % val
517        _gen_prefix = _gen_prefix().next
518
519        for kind, data, pos in stream:
520
521            if kind is START or kind is EMPTY:
522                tag, attrs = data
523
524                tagname = tag.localname
525                tagns = tag.namespace
526                if tagns:
527                    if tagns in namespaces:
528                        prefix = namespaces[tagns][-1]
529                        if prefix:
530                            tagname = u'%s:%s' % (prefix, tagname)
531                    else:
532                        _push_ns_attr((u'xmlns', tagns))
533                        _push_ns('', tagns)
534
535                new_attrs = []
536                for attr, value in attrs:
537                    attrname = attr.localname
538                    attrns = attr.namespace
539                    if attrns:
540                        if attrns not in namespaces:
541                            prefix = _gen_prefix()
542                            _push_ns(prefix, attrns)
543                            _push_ns_attr(('xmlns:%s' % prefix, attrns))
544                        else:
545                            prefix = namespaces[attrns][-1]
546                        if prefix:
547                            attrname = u'%s:%s' % (prefix, attrname)
548                    new_attrs.append((attrname, value))
549
550                yield kind, (tagname, Attrs(ns_attrs + new_attrs)), pos
551                del ns_attrs[:]
552
553            elif kind is END:
554                tagname = data.localname
555                tagns = data.namespace
556                if tagns:
557                    prefix = namespaces[tagns][-1]
558                    if prefix:
559                        tagname = u'%s:%s' % (prefix, tagname)
560                yield kind, tagname, pos
561
562            elif kind is START_NS:
563                prefix, uri = data
564                if uri not in namespaces:
565                    prefix = prefixes.get(uri, [prefix])[-1]
566                    _push_ns_attr(_make_ns_attr(prefix, uri))
567                _push_ns(prefix, uri)
568
569            elif kind is END_NS:
570                if data in prefixes:
571                    uris = prefixes.get(data)
572                    uri = uris.pop()
573                    if not uris:
574                        del prefixes[data]
575                    if uri not in uris or uri != uris[-1]:
576                        uri_prefixes = namespaces[uri]
577                        uri_prefixes.pop()
578                        if not uri_prefixes:
579                            del namespaces[uri]
580                    if ns_attrs:
581                        attr = _make_ns_attr(data, uri)
582                        if attr in ns_attrs:
583                            ns_attrs.remove(attr)
584
585            else:
586                yield kind, data, pos
587
588
589class NamespaceStripper(object):
590    r"""Stream filter that removes all namespace information from a stream, and
591    optionally strips out all tags not in a given namespace.
592   
593    :param namespace: the URI of the namespace that should not be stripped. If
594                      not set, only elements with no namespace are included in
595                      the output.
596   
597    >>> from genshi.input import XML
598    >>> xml = XML('''<doc xmlns="NS1" xmlns:two="NS2">
599    ...   <two:item/>
600    ... </doc>''')
601    >>> for kind, data, pos in NamespaceStripper(Namespace('NS1'))(xml):
602    ...     print kind, repr(data)
603    START (u'doc', Attrs())
604    TEXT u'\n  '
605    TEXT u'\n'
606    END u'doc'
607    """
608
609    def __init__(self, namespace=None):
610        if namespace is not None:
611            self.namespace = Namespace(namespace)
612        else:
613            self.namespace = {}
614
615    def __call__(self, stream):
616        namespace = self.namespace
617
618        for kind, data, pos in stream:
619
620            if kind is START or kind is EMPTY:
621                tag, attrs = data
622                if tag.namespace and tag not in namespace:
623                    continue
624
625                new_attrs = []
626                for attr, value in attrs:
627                    if not attr.namespace or attr in namespace:
628                        new_attrs.append((attr, value))
629
630                data = tag.localname, Attrs(new_attrs)
631
632            elif kind is END:
633                if data.namespace and data not in namespace:
634                    continue
635                data = data.localname
636
637            elif kind is START_NS or kind is END_NS:
638                continue
639
640            yield kind, data, pos
641
642
643class WhitespaceFilter(object):
644    """A filter that removes extraneous ignorable white space from the
645    stream.
646    """
647
648    def __init__(self, preserve=None, noescape=None):
649        """Initialize the filter.
650       
651        :param preserve: a set or sequence of tag names for which white-space
652                         should be preserved
653        :param noescape: a set or sequence of tag names for which text content
654                         should not be escaped
655       
656        The `noescape` set is expected to refer to elements that cannot contain
657        further child elements (such as ``<style>`` or ``<script>`` in HTML
658        documents).
659        """
660        if preserve is None:
661            preserve = []
662        self.preserve = frozenset(preserve)
663        if noescape is None:
664            noescape = []
665        self.noescape = frozenset(noescape)
666
667    def __call__(self, stream, ctxt=None, space=XML_NAMESPACE['space'],
668                 trim_trailing_space=re.compile('[ \t]+(?=\n)').sub,
669                 collapse_lines=re.compile('\n{2,}').sub):
670        mjoin = Markup('').join
671        preserve_elems = self.preserve
672        preserve = 0
673        noescape_elems = self.noescape
674        noescape = False
675
676        textbuf = []
677        push_text = textbuf.append
678        pop_text = textbuf.pop
679        for kind, data, pos in chain(stream, [(None, None, None)]):
680
681            if kind is TEXT:
682                if noescape:
683                    data = Markup(data)
684                push_text(data)
685            else:
686                if textbuf:
687                    if len(textbuf) > 1:
688                        text = mjoin(textbuf, escape_quotes=False)
689                        del textbuf[:]
690                    else:
691                        text = escape(pop_text(), quotes=False)
692                    if not preserve:
693                        text = collapse_lines('\n', trim_trailing_space('', text))
694                    yield TEXT, Markup(text), pos
695
696                if kind is START:
697                    tag, attrs = data
698                    if preserve or (tag in preserve_elems or
699                                    attrs.get(space) == 'preserve'):
700                        preserve += 1
701                    if not noescape and tag in noescape_elems:
702                        noescape = True
703
704                elif kind is END:
705                    noescape = False
706                    if preserve:
707                        preserve -= 1
708
709                elif kind is START_CDATA:
710                    noescape = True
711
712                elif kind is END_CDATA:
713                    noescape = False
714
715                if kind:
716                    yield kind, data, pos
Note: See TracBrowser for help on using the repository browser.