root/trunk/pylucid_project/PyLucid/tools/feedparser.py

Revision 1777, 120.1 kB (checked in by JensDiemer, 4 weeks ago)
  • set version to final v0.8.5
  • update install data
  • update SVN meta data
  • Property svn:eol-style set to LF
  • Property svn:executable set to *
Line 
1#!/usr/bin/env python
2"""Universal feed parser
3
4Handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds
5
6Visit http://feedparser.org/ for the latest version
7Visit http://feedparser.org/docs/ for the latest documentation
8
9Required: Python 2.1 or later
10Recommended: Python 2.3 or later
11Recommended: CJKCodecs and iconv_codec <http://cjkpython.i18n.org/>
12"""
13
14__version__ = "4.1"# + "$Revision: 1.92 $"[11:15] + "-cvs"
15__license__ = """Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved.
16
17Redistribution and use in source and binary forms, with or without modification,
18are permitted provided that the following conditions are met:
19
20* Redistributions of source code must retain the above copyright notice,
21  this list of conditions and the following disclaimer.
22* Redistributions in binary form must reproduce the above copyright notice,
23  this list of conditions and the following disclaimer in the documentation
24  and/or other materials provided with the distribution.
25
26THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
27AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
30LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36POSSIBILITY OF SUCH DAMAGE."""
37__author__ = "Mark Pilgrim <http://diveintomark.org/>"
38__contributors__ = ["Jason Diamond <http://injektilo.org/>",
39                    "John Beimler <http://john.beimler.org/>",
40                    "Fazal Majid <http://www.majid.info/mylos/weblog/>",
41                    "Aaron Swartz <http://aaronsw.com/>",
42                    "Kevin Marks <http://epeus.blogspot.com/>"]
43_debug = 0
44
45# HTTP "User-Agent" header to send to servers when downloading feeds.
46# If you are embedding feedparser in a larger application, you should
47# change this to your application name and URL.
48USER_AGENT = "UniversalFeedParser/%s +http://feedparser.org/" % __version__
49
50# HTTP "Accept" header to send to servers when downloading feeds.  If you don't
51# want to send an Accept header, set this to None.
52ACCEPT_HEADER = "application/atom+xml,application/rdf+xml,application/rss+xml,application/x-netcdf,application/xml;q=0.9,text/xml;q=0.2,*/*;q=0.1"
53
54# List of preferred XML parsers, by SAX driver name.  These will be tried first,
55# but if they're not installed, Python will keep searching through its own list
56# of pre-installed parsers until it finds one that supports everything we need.
57PREFERRED_XML_PARSERS = ["drv_libxml2"]
58
59# If you want feedparser to automatically run HTML markup through HTML Tidy, set
60# this to 1.  Requires mxTidy <http://www.egenix.com/files/python/mxTidy.html>
61# or utidylib <http://utidylib.berlios.de/>.
62TIDY_MARKUP = 0
63
64# List of Python interfaces for HTML Tidy, in order of preference.  Only useful
65# if TIDY_MARKUP = 1
66PREFERRED_TIDY_INTERFACES = ["uTidy", "mxTidy"]
67
68# ---------- required modules (should come with any Python distribution) ----------
69import sgmllib, re, sys, copy, urlparse, time, rfc822, types, cgi, urllib, urllib2
70try:
71    from cStringIO import StringIO as _StringIO
72except:
73    from StringIO import StringIO as _StringIO
74
75# ---------- optional modules (feedparser will work without these, but with reduced functionality) ----------
76
77# gzip is included with most Python distributions, but may not be available if you compiled your own
78try:
79    import gzip
80except:
81    gzip = None
82try:
83    import zlib
84except:
85    zlib = None
86
87# If a real XML parser is available, feedparser will attempt to use it.  feedparser has
88# been tested with the built-in SAX parser, PyXML, and libxml2.  On platforms where the
89# Python distribution does not come with an XML parser (such as Mac OS X 10.2 and some
90# versions of FreeBSD), feedparser will quietly fall back on regex-based parsing.
91try:
92    import xml.sax
93    xml.sax.make_parser(PREFERRED_XML_PARSERS) # test for valid parsers
94    from xml.sax.saxutils import escape as _xmlescape
95    _XML_AVAILABLE = 1
96except:
97    _XML_AVAILABLE = 0
98    def _xmlescape(data):
99        data = data.replace('&', '&amp;')
100        data = data.replace('>', '&gt;')
101        data = data.replace('<', '&lt;')
102        return data
103
104# base64 support for Atom feeds that contain embedded binary data
105try:
106    import base64, binascii
107except:
108    base64 = binascii = None
109
110# cjkcodecs and iconv_codec provide support for more character encodings.
111# Both are available from http://cjkpython.i18n.org/
112try:
113    import cjkcodecs.aliases
114except:
115    pass
116try:
117    import iconv_codec
118except:
119    pass
120
121# chardet library auto-detects character encodings
122# Download from http://chardet.feedparser.org/
123try:
124    import chardet
125    if _debug:
126        import chardet.constants
127        chardet.constants._debug = 1
128except:
129    chardet = None
130
131# ---------- don't touch these ----------
132class ThingsNobodyCaresAboutButMe(Exception): pass
133class CharacterEncodingOverride(ThingsNobodyCaresAboutButMe): pass
134class CharacterEncodingUnknown(ThingsNobodyCaresAboutButMe): pass
135class NonXMLContentType(ThingsNobodyCaresAboutButMe): pass
136class UndeclaredNamespace(Exception): pass
137
138sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
139sgmllib.special = re.compile('<!')
140sgmllib.charref = re.compile('&#(x?[0-9A-Fa-f]+)[^0-9A-Fa-f]')
141
142SUPPORTED_VERSIONS = {'': 'unknown',
143                      'rss090': 'RSS 0.90',
144                      'rss091n': 'RSS 0.91 (Netscape)',
145                      'rss091u': 'RSS 0.91 (Userland)',
146                      'rss092': 'RSS 0.92',
147                      'rss093': 'RSS 0.93',
148                      'rss094': 'RSS 0.94',
149                      'rss20': 'RSS 2.0',
150                      'rss10': 'RSS 1.0',
151                      'rss': 'RSS (unknown version)',
152                      'atom01': 'Atom 0.1',
153                      'atom02': 'Atom 0.2',
154                      'atom03': 'Atom 0.3',
155                      'atom10': 'Atom 1.0',
156                      'atom': 'Atom (unknown version)',
157                      'cdf': 'CDF',
158                      'hotrss': 'Hot RSS'
159                      }
160
161try:
162    UserDict = dict
163except NameError:
164    # Python 2.1 does not have dict
165    from UserDict import UserDict
166    def dict(aList):
167        rc = {}
168        for k, v in aList:
169            rc[k] = v
170        return rc
171
172class FeedParserDict(UserDict):
173    keymap = {'channel': 'feed',
174              'items': 'entries',
175              'guid': 'id',
176              'date': 'updated',
177              'date_parsed': 'updated_parsed',
178              'description': ['subtitle', 'summary'],
179              'url': ['href'],
180              'modified': 'updated',
181              'modified_parsed': 'updated_parsed',
182              'issued': 'published',
183              'issued_parsed': 'published_parsed',
184              'copyright': 'rights',
185              'copyright_detail': 'rights_detail',
186              'tagline': 'subtitle',
187              'tagline_detail': 'subtitle_detail'}
188    def __getitem__(self, key):
189        if key == 'category':
190            return UserDict.__getitem__(self, 'tags')[0]['term']
191        if key == 'categories':
192            return [(tag['scheme'], tag['term']) for tag in UserDict.__getitem__(self, 'tags')]
193        realkey = self.keymap.get(key, key)
194        if type(realkey) == types.ListType:
195            for k in realkey:
196                if UserDict.has_key(self, k):
197                    return UserDict.__getitem__(self, k)
198        if UserDict.has_key(self, key):
199            return UserDict.__getitem__(self, key)
200        return UserDict.__getitem__(self, realkey)
201
202    def __setitem__(self, key, value):
203        for k in self.keymap.keys():
204            if key == k:
205                key = self.keymap[k]
206                if type(key) == types.ListType:
207                    key = key[0]
208        return UserDict.__setitem__(self, key, value)
209
210    def get(self, key, default=None):
211        if self.has_key(key):
212            return self[key]
213        else:
214            return default
215
216    def setdefault(self, key, value):
217        if not self.has_key(key):
218            self[key] = value
219        return self[key]
220       
221    def has_key(self, key):
222        try:
223            return hasattr(self, key) or UserDict.has_key(self, key)
224        except AttributeError:
225            return False
226       
227    def __getattr__(self, key):
228        try:
229            return self.__dict__[key]
230        except KeyError:
231            pass
232        try:
233            assert not key.startswith('_')
234            return self.__getitem__(key)
235        except:
236            raise AttributeError, "object has no attribute '%s'" % key
237
238    def __setattr__(self, key, value):
239        if key.startswith('_') or key == 'data':
240            self.__dict__[key] = value
241        else:
242            return self.__setitem__(key, value)
243
244    def __contains__(self, key):
245        return self.has_key(key)
246
247def zopeCompatibilityHack():
248    global FeedParserDict
249    del FeedParserDict
250    def FeedParserDict(aDict=None):
251        rc = {}
252        if aDict:
253            rc.update(aDict)
254        return rc
255
256_ebcdic_to_ascii_map = None
257def _ebcdic_to_ascii(s):
258    global _ebcdic_to_ascii_map
259    if not _ebcdic_to_ascii_map:
260        emap = (
261            0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15,
262            16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31,
263            128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7,
264            144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26,
265            32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33,
266            38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94,
267            45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63,
268            186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34,
269            195,97,98,99,100,101,102,103,104,105,196,197,198,199,200,201,
270            202,106,107,108,109,110,111,112,113,114,203,204,205,206,207,208,
271            209,126,115,116,117,118,119,120,121,122,210,211,212,213,214,215,
272            216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,
273            123,65,66,67,68,69,70,71,72,73,232,233,234,235,236,237,
274            125,74,75,76,77,78,79,80,81,82,238,239,240,241,242,243,
275            92,159,83,84,85,86,87,88,89,90,244,245,246,247,248,249,
276            48,49,50,51,52,53,54,55,56,57,250,251,252,253,254,255
277            )
278        import string
279        _ebcdic_to_ascii_map = string.maketrans( \
280            ''.join(map(chr, range(256))), ''.join(map(chr, emap)))
281    return s.translate(_ebcdic_to_ascii_map)
282
283_urifixer = re.compile('^([A-Za-z][A-Za-z0-9+-.]*://)(/*)(.*?)')
284def _urljoin(base, uri):
285    uri = _urifixer.sub(r'\1\3', uri)
286    return urlparse.urljoin(base, uri)
287
288class _FeedParserMixin:
289    namespaces = {'': '',
290                  'http://backend.userland.com/rss': '',
291                  'http://blogs.law.harvard.edu/tech/rss': '',
292                  'http://purl.org/rss/1.0/': '',
293                  'http://my.netscape.com/rdf/simple/0.9/': '',
294                  'http://example.com/newformat#': '',
295                  'http://example.com/necho': '',
296                  'http://purl.org/echo/': '',
297                  'uri/of/echo/namespace#': '',
298                  'http://purl.org/pie/': '',
299                  'http://purl.org/atom/ns#': '',
300                  'http://www.w3.org/2005/Atom': '',
301                  'http://purl.org/rss/1.0/modules/rss091#': '',
302                 
303                  'http://webns.net/mvcb/':                               'admin',
304                  'http://purl.org/rss/1.0/modules/aggregation/':         'ag',
305                  'http://purl.org/rss/1.0/modules/annotate/':            'annotate',
306                  'http://media.tangent.org/rss/1.0/':                    'audio',
307                  'http://backend.userland.com/blogChannelModule':        'blogChannel',
308                  'http://web.resource.org/cc/':                          'cc',
309                  'http://backend.userland.com/creativeCommonsRssModule': 'creativeCommons',
310                  'http://purl.org/rss/1.0/modules/company':              'co',
311                  'http://purl.org/rss/1.0/modules/content/':             'content',
312                  'http://my.theinfo.org/changed/1.0/rss/':               'cp',
313                  'http://purl.org/dc/elements/1.1/':                     'dc',
314                  'http://purl.org/dc/terms/':                            'dcterms',
315                  'http://purl.org/rss/1.0/modules/email/':               'email',
316                  'http://purl.org/rss/1.0/modules/event/':               'ev',
317                  'http://rssnamespace.org/feedburner/ext/1.0':           'feedburner',
318                  'http://freshmeat.net/rss/fm/':                         'fm',
319                  'http://xmlns.com/foaf/0.1/':                           'foaf',
320                  'http://www.w3.org/2003/01/geo/wgs84_pos#':             'geo',
321                  'http://postneo.com/icbm/':                             'icbm',
322                  'http://purl.org/rss/1.0/modules/image/':               'image',
323                  'http://www.itunes.com/DTDs/PodCast-1.0.dtd':           'itunes',
324                  'http://example.com/DTDs/PodCast-1.0.dtd':              'itunes',
325                  'http://purl.org/rss/1.0/modules/link/':                'l',
326                  'http://search.yahoo.com/mrss':                         'media',
327                  'http://madskills.com/public/xml/rss/module/pingback/': 'pingback',
328                  'http://prismstandard.org/namespaces/1.2/basic/':       'prism',
329                  'http://www.w3.org/1999/02/22-rdf-syntax-ns#':          'rdf',
330                  'http://www.w3.org/2000/01/rdf-schema#':                'rdfs',
331                  'http://purl.org/rss/1.0/modules/reference/':           'ref',
332                  'http://purl.org/rss/1.0/modules/richequiv/':           'reqv',
333                  'http://purl.org/rss/1.0/modules/search/':              'search',
334                  'http://purl.org/rss/1.0/modules/slash/':               'slash',
335                  'http://schemas.xmlsoap.org/soap/envelope/':            'soap',
336                  'http://purl.org/rss/1.0/modules/servicestatus/':       'ss',
337                  'http://hacks.benhammersley.com/rss/streaming/':        'str',
338                  'http://purl.org/rss/1.0/modules/subscription/':        'sub',
339                  'http://purl.org/rss/1.0/modules/syndication/':         'sy',
340                  'http://purl.org/rss/1.0/modules/taxonomy/':            'taxo',
341                  'http://purl.org/rss/1.0/modules/threading/':           'thr',
342                  'http://purl.org/rss/1.0/modules/textinput/':           'ti',
343                  'http://madskills.com/public/xml/rss/module/trackback/':'trackback',
344                  'http://wellformedweb.org/commentAPI/':                 'wfw',
345                  'http://purl.org/rss/1.0/modules/wiki/':                'wiki',
346                  'http://www.w3.org/1999/xhtml':                         'xhtml',
347                  'http://www.w3.org/XML/1998/namespace':                 'xml',
348                  'http://schemas.pocketsoap.com/rss/myDescModule/':      'szf'
349}
350    _matchnamespaces = {}
351
352    can_be_relative_uri = ['link', 'id', 'wfw_comment', 'wfw_commentrss', 'docs', 'url', 'href', 'comments', 'license', 'icon', 'logo']
353    can_contain_relative_uris = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description']
354    can_contain_dangerous_markup = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description']
355    html_types = ['text/html', 'application/xhtml+xml']
356   
357    def __init__(self, baseuri=None, baselang=None, encoding='utf-8'):
358        if _debug: sys.stderr.write('initializing FeedParser\n')
359        if not self._matchnamespaces:
360            for k, v in self.namespaces.items():
361                self._matchnamespaces[k.lower()] = v
362        self.feeddata = FeedParserDict() # feed-level data
363        self.encoding = encoding # character encoding
364        self.entries = [] # list of entry-level data
365        self.version = '' # feed type/version, see SUPPORTED_VERSIONS
366        self.namespacesInUse = {} # dictionary of namespaces defined by the feed
367
368        # the following are used internally to track state;
369        # this is really out of control and should be refactored
370        self.infeed = 0
371        self.inentry = 0
372        self.incontent = 0
373        self.intextinput = 0
374        self.inimage = 0
375        self.inauthor = 0
376        self.incontributor = 0
377        self.inpublisher = 0
378        self.insource = 0
379        self.sourcedata = FeedParserDict()
380        self.contentparams = FeedParserDict()
381        self._summaryKey = None
382        self.namespacemap = {}
383        self.elementstack = []
384        self.basestack = []
385        self.langstack = []
386        self.baseuri = baseuri or ''
387        self.lang = baselang or None
388        if baselang:
389            self.feeddata['language'] = baselang
390
391    def unknown_starttag(self, tag, attrs):
392        if _debug: sys.stderr.write('start %s with %s\n' % (tag, attrs))
393        # normalize attrs
394        attrs = [(k.lower(), v) for k, v in attrs]
395        attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs]
396       
397        # track xml:base and xml:lang
398        attrsD = dict(attrs)
399        baseuri = attrsD.get('xml:base', attrsD.get('base')) or self.baseuri
400        self.baseuri = _urljoin(self.baseuri, baseuri)
401        lang = attrsD.get('xml:lang', attrsD.get('lang'))
402        if lang == '':
403            # xml:lang could be explicitly set to '', we need to capture that
404            lang = None
405        elif lang is None:
406            # if no xml:lang is specified, use parent lang
407            lang = self.lang
408        if lang:
409            if tag in ('feed', 'rss', 'rdf:RDF'):
410                self.feeddata['language'] = lang
411        self.lang = lang
412        self.basestack.append(self.baseuri)
413        self.langstack.append(lang)
414       
415        # track namespaces
416        for prefix, uri in attrs:
417            if prefix.startswith('xmlns:'):
418                self.trackNamespace(prefix[6:], uri)
419            elif prefix == 'xmlns':
420                self.trackNamespace(None, uri)
421
422        # track inline content
423        if self.incontent and self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
424            # element declared itself as escaped markup, but it isn't really
425            self.contentparams['type'] = 'application/xhtml+xml'
426        if self.incontent and self.contentparams.get('type') == 'application/xhtml+xml':
427            # Note: probably shouldn't simply recreate localname here, but
428            # our namespace handling isn't actually 100% correct in cases where
429            # the feed redefines the default namespace (which is actually
430            # the usual case for inline content, thanks Sam), so here we
431            # cheat and just reconstruct the element based on localname
432            # because that compensates for the bugs in our namespace handling.
433            # This will horribly munge inline content with non-empty qnames,
434            # but nobody actually does that, so I'm not fixing it.
435            tag = tag.split(':')[-1]
436            return self.handle_data('<%s%s>' % (tag, ''.join([' %s="%s"' % t for t in attrs])), escape=0)
437
438        # match namespaces
439        if tag.find(':') <> -1:
440            prefix, suffix = tag.split(':', 1)
441        else:
442            prefix, suffix = '', tag
443        prefix = self.namespacemap.get(prefix, prefix)
444        if prefix:
445            prefix = prefix + '_'
446
447        # special hack for better tracking of empty textinput/image elements in illformed feeds
448        if (not prefix) and tag not in ('title', 'link', 'description', 'name'):
449            self.intextinput = 0
450        if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'):
451            self.inimage = 0
452       
453        # call special handler (if defined) or default handler
454        methodname = '_start_' + prefix + suffix
455        try:
456            method = getattr(self, methodname)
457            return method(attrsD)
458        except AttributeError:
459            return self.push(prefix + suffix, 1)
460
461    def unknown_endtag(self, tag):
462        if _debug: sys.stderr.write('end %s\n' % tag)
463        # match namespaces
464        if tag.find(':') <> -1:
465            prefix, suffix = tag.split(':', 1)
466        else:
467            prefix, suffix = '', tag
468        prefix = self.namespacemap.get(prefix, prefix)
469        if prefix:
470            prefix = prefix + '_'
471
472        # call special handler (if defined) or default handler
473        methodname = '_end_' + prefix + suffix
474        try:
475            method = getattr(self, methodname)
476            method()
477        except AttributeError:
478            self.pop(prefix + suffix)
479
480        # track inline content
481        if self.incontent and self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
482            # element declared itself as escaped markup, but it isn't really
483            self.contentparams['type'] = 'application/xhtml+xml'
484        if self.incontent and self.contentparams.get('type') == 'application/xhtml+xml':
485            tag = tag.split(':')[-1]
486            self.handle_data('</%s>' % tag, escape=0)
487
488        # track xml:base and xml:lang going out of scope
489        if self.basestack:
490            self.basestack.pop()
491            if self.basestack and self.basestack[-1]:
492                self.baseuri = self.basestack[-1]
493        if self.langstack:
494            self.langstack.pop()
495            if self.langstack: # and (self.langstack[-1] is not None):
496                self.lang = self.langstack[-1]
497
498    def handle_charref(self, ref):
499        # called for each character reference, e.g. for '&#160;', ref will be '160'
500        if not self.elementstack: return
501        ref = ref.lower()
502        if ref in ('34', '38', '39', '60', '62', 'x22', 'x26', 'x27', 'x3c', 'x3e'):
503            text = '&#%s;' % ref
504        else:
505            if ref[0] == 'x':
506                c = int(ref[1:], 16)
507            else:
508                c = int(ref)
509            text = unichr(c).encode('utf-8')
510        self.elementstack[-1][2].append(text)
511
512    def handle_entityref(self, ref):
513        # called for each entity reference, e.g. for '&copy;', ref will be 'copy'
514        if not self.elementstack: return
515        if _debug: sys.stderr.write('entering handle_entityref with %s\n' % ref)
516        if ref in ('lt', 'gt', 'quot', 'amp', 'apos'):
517            text = '&%s;' % ref
518        else:
519            # entity resolution graciously donated by Aaron Swartz
520            def name2cp(k):
521                import htmlentitydefs
522                if hasattr(htmlentitydefs, 'name2codepoint'): # requires Python 2.3
523                    return htmlentitydefs.name2codepoint[k]
524                k = htmlentitydefs.entitydefs[k]
525                if k.startswith('&#') and k.endswith(';'):
526                    return int(k[2:-1]) # not in latin-1
527                return ord(k)
528            try: name2cp(ref)
529            except KeyError: text = '&%s;' % ref
530            else: text = unichr(name2cp(ref)).encode('utf-8')
531        self.elementstack[-1][2].append(text)
532
533    def handle_data(self, text, escape=1):
534        # called for each block of plain text, i.e. outside of any tag and
535        # not containing any character or entity references
536        if not self.elementstack: return
537        if escape and self.contentparams.get('type') == 'application/xhtml+xml':
538            text = _xmlescape(text)
539        self.elementstack[-1][2].append(text)
540
541    def handle_comment(self, text):
542        # called for each comment, e.g. <!-- insert message here -->
543        pass
544
545    def handle_pi(self, text):
546        # called for each processing instruction, e.g. <?instruction>
547        pass
548
549    def handle_decl(self, text):
550        pass
551
552    def parse_declaration(self, i):
553        # override internal declaration handler to handle CDATA blocks
554        if _debug: sys.stderr.write('entering parse_declaration\n')
555        if self.rawdata[i:i+9] == '<![CDATA[':
556            k = self.rawdata.find(']]>', i)
557            if k == -1: k = len(self.rawdata)
558            self.handle_data(_xmlescape(self.rawdata[i+9:k]), 0)
559            return k+3
560        else:
561            k = self.rawdata.find