and
# only if all the remaining content is nested underneath it.
# This means that the divs would be retained in the following:
@@ -805,7 +854,8 @@ class _FeedParserMixin:
for piece in pieces[:-1]:
if piece.startswith(''):
depth -= 1
- if depth == 0: break
+ if depth == 0:
+ break
elif piece.startswith('<') and not piece.endswith('/>'):
depth += 1
else:
@@ -813,13 +863,14 @@ class _FeedParserMixin:
# Ensure each piece is a str for Python 3
for (i, v) in enumerate(pieces):
- if not isinstance(v, basestring):
+ if not isinstance(v, str):
pieces[i] = v.decode('utf-8')
output = ''.join(pieces)
if stripWhitespace:
output = output.strip()
- if not expectingText: return output
+ if not expectingText:
+ return output
# decode base64 content
if base64 and self.contentparams.get('base64', 0):
@@ -836,14 +887,19 @@ class _FeedParserMixin:
# resolve relative URIs
if (element in self.can_be_relative_uri) and output:
- output = self.resolveURI(output)
+ # do not resolve guid elements with isPermalink="false"
+ if not element == 'id' or self.guidislink:
+ output = self.resolveURI(output)
# decode entities within embedded markup
if not self.contentparams.get('base64', 0):
output = self.decodeEntities(element, output)
- if self.lookslikehtml(output):
- self.contentparams['type']='text/html'
+ # some feed formats require consumers to guess
+ # whether the content is html or plain text
+ if not self.version.startswith('atom') and self.contentparams.get('type') == 'text/plain':
+ if self.lookslikehtml(output):
+ self.contentparams['type'] = 'text/html'
# remove temporary cruft from contentparams
try:
@@ -861,50 +917,31 @@ class _FeedParserMixin:
if element in self.can_contain_relative_uris:
output = _resolveRelativeURIs(output, self.baseuri, self.encoding, self.contentparams.get('type', 'text/html'))
- # parse microformats
- # (must do this before sanitizing because some microformats
- # rely on elements that we sanitize)
- if is_htmlish and element in ['content', 'description', 'summary']:
- mfresults = _parseMicroformats(output, self.baseuri, self.encoding)
- if mfresults:
- for tag in mfresults.get('tags', []):
- self._addTag(tag['term'], tag['scheme'], tag['label'])
- for enclosure in mfresults.get('enclosures', []):
- self._start_enclosure(enclosure)
- for xfn in mfresults.get('xfn', []):
- self._addXFN(xfn['relationships'], xfn['href'], xfn['name'])
- vcard = mfresults.get('vcard')
- if vcard:
- self._getContext()['vcard'] = vcard
-
# sanitize embedded markup
if is_htmlish and SANITIZE_HTML:
if element in self.can_contain_dangerous_markup:
output = _sanitizeHTML(output, self.encoding, self.contentparams.get('type', 'text/html'))
- if self.encoding and type(output) != type(u''):
- try:
- output = unicode(output, self.encoding)
- except:
- pass
+ if self.encoding and not isinstance(output, str):
+ output = output.decode(self.encoding, 'ignore')
# address common error where people take data that is already
# utf-8, presume that it is iso-8859-1, and re-encode it.
- if self.encoding in ('utf-8', 'utf-8_INVALID_PYTHON_3') and type(output) == type(u''):
+ if self.encoding in ('utf-8', 'utf-8_INVALID_PYTHON_3') and isinstance(output, str):
try:
- output = unicode(output.encode('iso-8859-1'), 'utf-8')
- except:
+ output = output.encode('iso-8859-1').decode('utf-8')
+ except (UnicodeEncodeError, UnicodeDecodeError):
pass
# map win-1252 extensions to the proper code points
- if type(output) == type(u''):
- output = u''.join([c in _cp1252.keys() and _cp1252[c] or c for c in output])
+ if isinstance(output, str):
+ output = output.translate(_cp1252)
- # categories/tags/keywords/whatever are handled in _end_category
- if element == 'category':
+ # categories/tags/keywords/whatever are handled in _end_category or _end_tags or _end_itunes_keywords
+ if element in ('category', 'tags', 'itunes_keywords'):
return output
- if element == 'title' and self.hasTitle:
+ if element == 'title' and -1 < self.title_depth <= self.depth:
return output
# store output in appropriate place(s)
@@ -919,6 +956,7 @@ class _FeedParserMixin:
# query variables in urls in link elements are improperly
# converted from `?a=1&b=2` to `?a=1&b;=2` as if they're
# unhandled character references. fix this special case.
+ output = output.replace('&', '&')
output = re.sub("&([A-Za-z0-9_]+);", "&\g<1>", output)
self.entries[-1][element] = output
if output:
@@ -926,7 +964,10 @@ class _FeedParserMixin:
else:
if element == 'description':
element = 'summary'
- self.entries[-1][element] = output
+ old_value_depth = self.property_depth_map.setdefault(self.entries[-1], {}).get(element)
+ if old_value_depth is None or self.depth <= old_value_depth:
+ self.property_depth_map[self.entries[-1]][element] = self.depth
+ self.entries[-1][element] = output
if self.incontent:
contentparams = copy.deepcopy(self.contentparams)
contentparams['value'] = output
@@ -949,7 +990,8 @@ class _FeedParserMixin:
def pushContent(self, tag, attrsD, defaultContentType, expectingText):
self.incontent += 1
- if self.lang: self.lang=self.lang.replace('_','-')
+ if self.lang:
+ self.lang=self.lang.replace('_','-')
self.contentparams = FeedParserDict({
'type': self.mapContentType(attrsD.get('type', defaultContentType)),
'language': self.lang,
@@ -967,27 +1009,25 @@ class _FeedParserMixin:
# text, but this is routinely ignored. This is an attempt to detect
# the most common cases. As false positives often result in silent
# data loss, this function errs on the conservative side.
- def lookslikehtml(self, s):
- if self.version.startswith('atom'): return
- if self.contentparams.get('type','text/html') != 'text/plain': return
-
- # must have a close tag or a entity reference to qualify
- if not (re.search(r'(\w+)>',s) or re.search("?\w+;",s)): return
+ @staticmethod
+ def lookslikehtml(s):
+ # must have a close tag or an entity reference to qualify
+ if not (re.search(r'(\w+)>',s) or re.search("?\w+;",s)):
+ return
# all tags must be in a restricted subset of valid HTML tags
- if filter(lambda t: t.lower() not in _HTMLSanitizer.acceptable_elements,
- re.findall(r'?(\w+)',s)): return
+ if [t for t in re.findall(r'?(\w+)',s) if t.lower() not in _HTMLSanitizer.acceptable_elements]:
+ return
# all entities must have been defined as valid HTML entities
- from htmlentitydefs import entitydefs
- if filter(lambda e: e not in entitydefs.keys(),
- re.findall(r'&(\w+);',s)): return
+ if [e for e in re.findall(r'&(\w+);', s) if e not in list(entitydefs.keys())]:
+ return
return 1
def _mapToStandardPrefix(self, name):
colonpos = name.find(':')
- if colonpos <> -1:
+ if colonpos != -1:
prefix = name[:colonpos]
suffix = name[colonpos+1:]
prefix = self.namespacemap.get(prefix, prefix)
@@ -1047,20 +1087,16 @@ class _FeedParserMixin:
else:
self.version = 'rss'
- def _start_dlhottitles(self, attrsD):
- self.version = 'hotrss'
-
def _start_channel(self, attrsD):
self.infeed = 1
self._cdf_common(attrsD)
- _start_feedinfo = _start_channel
def _cdf_common(self, attrsD):
- if attrsD.has_key('lastmod'):
+ if 'lastmod' in attrsD:
self._start_modified({})
self.elementstack[-1][-1] = attrsD['lastmod']
self._end_modified()
- if attrsD.has_key('href'):
+ if 'href' in attrsD:
self._start_link({})
self.elementstack[-1][-1] = attrsD['href']
self._end_link()
@@ -1087,7 +1123,7 @@ class _FeedParserMixin:
if not self.inentry:
context.setdefault('image', FeedParserDict())
self.inimage = 1
- self.hasTitle = 0
+ self.title_depth = -1
self.push('image', 0)
def _end_image(self):
@@ -1098,7 +1134,7 @@ class _FeedParserMixin:
context = self._getContext()
context.setdefault('textinput', FeedParserDict())
self.intextinput = 1
- self.hasTitle = 0
+ self.title_depth = -1
self.push('textinput', 0)
_start_textInput = _start_textinput
@@ -1183,7 +1219,7 @@ class _FeedParserMixin:
value = self.pop('width')
try:
value = int(value)
- except:
+ except ValueError:
value = 0
if self.inimage:
context = self._getContext()
@@ -1196,7 +1232,7 @@ class _FeedParserMixin:
value = self.pop('height')
try:
value = int(value)
- except:
+ except ValueError:
value = 0
if self.inimage:
context = self._getContext()
@@ -1233,7 +1269,7 @@ class _FeedParserMixin:
def _getContext(self):
if self.insource:
context = self.sourcedata
- elif self.inimage and self.feeddata.has_key('image'):
+ elif self.inimage and 'image' in self.feeddata:
context = self.feeddata['image']
elif self.intextinput:
context = self.feeddata['textinput']
@@ -1258,7 +1294,7 @@ class _FeedParserMixin:
def _sync_author_detail(self, key='author'):
context = self._getContext()
- detail = context.get('%s_detail' % key)
+ detail = context.get('%ss' % key, [FeedParserDict()])[-1]
if detail:
name = detail.get('name')
email = detail.get('email')
@@ -1270,7 +1306,8 @@ class _FeedParserMixin:
context[key] = email
else:
author, email = context.get(key), None
- if not author: return
+ if not author:
+ return
emailmatch = re.search(r'''(([a-zA-Z0-9\_\-\.\+]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?))(\?subject=\S+)?''', author)
if emailmatch:
email = emailmatch.group(0)
@@ -1286,11 +1323,11 @@ class _FeedParserMixin:
author = author[:-1]
author = author.strip()
if author or email:
- context.setdefault('%s_detail' % key, FeedParserDict())
+ context.setdefault('%s_detail' % key, detail)
if author:
- context['%s_detail' % key]['name'] = author
+ detail['name'] = author
if email:
- context['%s_detail' % key]['email'] = email
+ detail['email'] = email
def _start_subtitle(self, attrsD):
self.pushContent('subtitle', attrsD, 'text/plain', 1)
@@ -1317,14 +1354,14 @@ class _FeedParserMixin:
self.push('item', 0)
self.inentry = 1
self.guidislink = 0
- self.hasTitle = 0
+ self.title_depth = -1
+ self.psc_chapters_flag = None
id = self._getAttribute(attrsD, 'rdf:about')
if id:
context = self._getContext()
context['id'] = id
self._cdf_common(attrsD)
_start_entry = _start_item
- _start_product = _start_item
def _end_item(self):
self.pop('item')
@@ -1348,22 +1385,37 @@ class _FeedParserMixin:
self._sync_author_detail('publisher')
_end_webmaster = _end_dc_publisher
+ def _start_dcterms_valid(self, attrsD):
+ self.push('validity', 1)
+
+ def _end_dcterms_valid(self):
+ for validity_detail in self.pop('validity').split(';'):
+ if '=' in validity_detail:
+ key, value = validity_detail.split('=', 1)
+ if key == 'start':
+ self._save('validity_start', value, overwrite=True)
+ self._save('validity_start_parsed', _parse_date(value), overwrite=True)
+ elif key == 'end':
+ self._save('validity_end', value, overwrite=True)
+ self._save('validity_end_parsed', _parse_date(value), overwrite=True)
+
def _start_published(self, attrsD):
self.push('published', 1)
_start_dcterms_issued = _start_published
_start_issued = _start_published
+ _start_pubdate = _start_published
def _end_published(self):
value = self.pop('published')
self._save('published_parsed', _parse_date(value), overwrite=True)
_end_dcterms_issued = _end_published
_end_issued = _end_published
+ _end_pubdate = _end_published
def _start_updated(self, attrsD):
self.push('updated', 1)
_start_modified = _start_updated
_start_dcterms_modified = _start_updated
- _start_pubdate = _start_updated
_start_dc_date = _start_updated
_start_lastbuilddate = _start_updated
@@ -1373,7 +1425,6 @@ class _FeedParserMixin:
self._save('updated_parsed', parsed_value, overwrite=True)
_end_modified = _end_updated
_end_dcterms_modified = _end_updated
- _end_pubdate = _end_updated
_end_dc_date = _end_updated
_end_lastbuilddate = _end_updated
@@ -1392,12 +1443,135 @@ class _FeedParserMixin:
def _end_expirationdate(self):
self._save('expired_parsed', _parse_date(self.pop('expired')), overwrite=True)
+ # geospatial location, or "where", from georss.org
+
+ def _start_georssgeom(self, attrsD):
+ self.push('geometry', 0)
+ context = self._getContext()
+ context['where'] = FeedParserDict()
+
+ _start_georss_point = _start_georssgeom
+ _start_georss_line = _start_georssgeom
+ _start_georss_polygon = _start_georssgeom
+ _start_georss_box = _start_georssgeom
+
+ def _save_where(self, geometry):
+ context = self._getContext()
+ context['where'].update(geometry)
+
+ def _end_georss_point(self):
+ geometry = _parse_georss_point(self.pop('geometry'))
+ if geometry:
+ self._save_where(geometry)
+
+ def _end_georss_line(self):
+ geometry = _parse_georss_line(self.pop('geometry'))
+ if geometry:
+ self._save_where(geometry)
+
+ def _end_georss_polygon(self):
+ this = self.pop('geometry')
+ geometry = _parse_georss_polygon(this)
+ if geometry:
+ self._save_where(geometry)
+
+ def _end_georss_box(self):
+ geometry = _parse_georss_box(self.pop('geometry'))
+ if geometry:
+ self._save_where(geometry)
+
+ def _start_where(self, attrsD):
+ self.push('where', 0)
+ context = self._getContext()
+ context['where'] = FeedParserDict()
+ _start_georss_where = _start_where
+
+ def _parse_srs_attrs(self, attrsD):
+ srsName = attrsD.get('srsname')
+ try:
+ srsDimension = int(attrsD.get('srsdimension', '2'))
+ except ValueError:
+ srsDimension = 2
+ context = self._getContext()
+ context['where']['srsName'] = srsName
+ context['where']['srsDimension'] = srsDimension
+
+ def _start_gml_point(self, attrsD):
+ self._parse_srs_attrs(attrsD)
+ self.ingeometry = 1
+ self.push('geometry', 0)
+
+ def _start_gml_linestring(self, attrsD):
+ self._parse_srs_attrs(attrsD)
+ self.ingeometry = 'linestring'
+ self.push('geometry', 0)
+
+ def _start_gml_polygon(self, attrsD):
+ self._parse_srs_attrs(attrsD)
+ self.push('geometry', 0)
+
+ def _start_gml_exterior(self, attrsD):
+ self.push('geometry', 0)
+
+ def _start_gml_linearring(self, attrsD):
+ self.ingeometry = 'polygon'
+ self.push('geometry', 0)
+
+ def _start_gml_pos(self, attrsD):
+ self.push('pos', 0)
+
+ def _end_gml_pos(self):
+ this = self.pop('pos')
+ context = self._getContext()
+ srsName = context['where'].get('srsName')
+ srsDimension = context['where'].get('srsDimension', 2)
+ swap = True
+ if srsName and "EPSG" in srsName:
+ epsg = int(srsName.split(":")[-1])
+ swap = bool(epsg in _geogCS)
+ geometry = _parse_georss_point(this, swap=swap, dims=srsDimension)
+ if geometry:
+ self._save_where(geometry)
+
+ def _start_gml_poslist(self, attrsD):
+ self.push('pos', 0)
+
+ def _end_gml_poslist(self):
+ this = self.pop('pos')
+ context = self._getContext()
+ srsName = context['where'].get('srsName')
+ srsDimension = context['where'].get('srsDimension', 2)
+ swap = True
+ if srsName and "EPSG" in srsName:
+ epsg = int(srsName.split(":")[-1])
+ swap = bool(epsg in _geogCS)
+ geometry = _parse_poslist(
+ this, self.ingeometry, swap=swap, dims=srsDimension)
+ if geometry:
+ self._save_where(geometry)
+
+ def _end_geom(self):
+ self.ingeometry = 0
+ self.pop('geometry')
+ _end_gml_point = _end_geom
+ _end_gml_linestring = _end_geom
+ _end_gml_linearring = _end_geom
+ _end_gml_exterior = _end_geom
+ _end_gml_polygon = _end_geom
+
+ def _end_where(self):
+ self.pop('where')
+ _end_georss_where = _end_where
+
+ # end geospatial
+
def _start_cc_license(self, attrsD):
context = self._getContext()
value = self._getAttribute(attrsD, 'rdf:resource')
attrsD = FeedParserDict()
- attrsD['rel']='license'
- if value: attrsD['href']=value
+ attrsD['rel'] = 'license'
+ if value:
+ attrsD['href']=value
context.setdefault('links', []).append(attrsD)
def _start_creativecommons_license(self, attrsD):
@@ -1408,29 +1582,33 @@ class _FeedParserMixin:
value = self.pop('license')
context = self._getContext()
attrsD = FeedParserDict()
- attrsD['rel']='license'
- if value: attrsD['href']=value
+ attrsD['rel'] = 'license'
+ if value:
+ attrsD['href'] = value
context.setdefault('links', []).append(attrsD)
del context['license']
_end_creativeCommons_license = _end_creativecommons_license
- def _addXFN(self, relationships, href, name):
- context = self._getContext()
- xfn = context.setdefault('xfn', [])
- value = FeedParserDict({'relationships': relationships, 'href': href, 'name': name})
- if value not in xfn:
- xfn.append(value)
-
def _addTag(self, term, scheme, label):
context = self._getContext()
tags = context.setdefault('tags', [])
- if (not term) and (not scheme) and (not label): return
- value = FeedParserDict({'term': term, 'scheme': scheme, 'label': label})
+ if (not term) and (not scheme) and (not label):
+ return
+ value = FeedParserDict(term=term, scheme=scheme, label=label)
if value not in tags:
tags.append(value)
+ def _start_tags(self, attrsD):
+ # This is a completely-made up element. Its semantics are determined
+ # only by a single feed that precipitated bug report 392 on Google Code.
+ # In short, this is junk code.
+ self.push('tags', 1)
+
+ def _end_tags(self):
+ for term in self.pop('tags').split(','):
+ self._addTag(term.strip(), None, None)
+
def _start_category(self, attrsD):
- if _debug: sys.stderr.write('entering _start_category with %s\n' % repr(attrsD))
term = attrsD.get('term')
scheme = attrsD.get('scheme', attrsD.get('domain'))
label = attrsD.get('label')
@@ -1444,8 +1622,14 @@ class _FeedParserMixin:
self._start_category(attrsD)
def _end_itunes_keywords(self):
- for term in self.pop('itunes_keywords').split():
- self._addTag(term, 'http://www.itunes.com/', None)
+ for term in self.pop('itunes_keywords').split(','):
+ if term.strip():
+ self._addTag(term.strip(), 'http://www.itunes.com/', None)
+
+ def _end_media_keywords(self):
+ for term in self.pop('media_keywords').split(','):
+ if term.strip():
+ self._addTag(term.strip(), None, None)
def _start_itunes_category(self, attrsD):
self._addTag(attrsD.get('text'), 'http://www.itunes.com/', None)
@@ -1453,7 +1637,8 @@ class _FeedParserMixin:
def _end_category(self):
value = self.pop('category')
- if not value: return
+ if not value:
+ return
context = self._getContext()
tags = context['tags']
if value and len(tags) and not tags[-1]['term']:
@@ -1476,64 +1661,66 @@ class _FeedParserMixin:
attrsD.setdefault('type', 'text/html')
context = self._getContext()
attrsD = self._itsAnHrefDamnIt(attrsD)
- if attrsD.has_key('href'):
+ if 'href' in attrsD:
attrsD['href'] = self.resolveURI(attrsD['href'])
expectingText = self.infeed or self.inentry or self.insource
context.setdefault('links', [])
if not (self.inentry and self.inimage):
context['links'].append(FeedParserDict(attrsD))
- if attrsD.has_key('href'):
+ if 'href' in attrsD:
expectingText = 0
if (attrsD.get('rel') == 'alternate') and (self.mapContentType(attrsD.get('type')) in self.html_types):
context['link'] = attrsD['href']
else:
self.push('link', expectingText)
- _start_producturl = _start_link
def _end_link(self):
value = self.pop('link')
- context = self._getContext()
- _end_producturl = _end_link
def _start_guid(self, attrsD):
self.guidislink = (attrsD.get('ispermalink', 'true') == 'true')
self.push('id', 1)
+ _start_id = _start_guid
def _end_guid(self):
value = self.pop('id')
- self._save('guidislink', self.guidislink and not self._getContext().has_key('link'))
+ self._save('guidislink', self.guidislink and 'link' not in self._getContext())
if self.guidislink:
# guid acts as link, but only if 'ispermalink' is not present or is 'true',
# and only if the item doesn't already have a link element
self._save('link', value)
+ _end_id = _end_guid
def _start_title(self, attrsD):
- if self.svgOK: return self.unknown_starttag('title', attrsD.items())
+ if self.svgOK:
+ return self.unknown_starttag('title', list(attrsD.items()))
self.pushContent('title', attrsD, 'text/plain', self.infeed or self.inentry or self.insource)
_start_dc_title = _start_title
_start_media_title = _start_title
def _end_title(self):
- if self.svgOK: return
+ if self.svgOK:
+ return
value = self.popContent('title')
- if not value: return
- context = self._getContext()
- self.hasTitle = 1
+ if not value:
+ return
+ self.title_depth = self.depth
_end_dc_title = _end_title
def _end_media_title(self):
- hasTitle = self.hasTitle
+ title_depth = self.title_depth
self._end_title()
- self.hasTitle = hasTitle
+ self.title_depth = title_depth
def _start_description(self, attrsD):
context = self._getContext()
- if context.has_key('summary'):
+ if 'summary' in context:
self._summaryKey = 'content'
self._start_content(attrsD)
else:
self.pushContent('description', attrsD, 'text/html', self.infeed or self.inentry or self.insource)
_start_dc_description = _start_description
+ _start_media_description = _start_description
def _start_abstract(self, attrsD):
self.pushContent('description', attrsD, 'text/plain', self.infeed or self.inentry or self.insource)
@@ -1546,6 +1733,7 @@ class _FeedParserMixin:
self._summaryKey = None
_end_abstract = _end_description
_end_dc_description = _end_description
+ _end_media_description = _end_description
def _start_info(self, attrsD):
self.pushContent('info', attrsD, 'text/plain', 1)
@@ -1558,7 +1746,7 @@ class _FeedParserMixin:
def _start_generator(self, attrsD):
if attrsD:
attrsD = self._itsAnHrefDamnIt(attrsD)
- if attrsD.has_key('href'):
+ if 'href' in attrsD:
attrsD['href'] = self.resolveURI(attrsD['href'])
self._getContext()['generator_detail'] = FeedParserDict(attrsD)
self.push('generator', 1)
@@ -1566,7 +1754,7 @@ class _FeedParserMixin:
def _end_generator(self):
value = self.pop('generator')
context = self._getContext()
- if context.has_key('generator_detail'):
+ if 'generator_detail' in context:
context['generator_detail']['name'] = value
def _start_admin_generatoragent(self, attrsD):
@@ -1586,7 +1774,7 @@ class _FeedParserMixin:
def _start_summary(self, attrsD):
context = self._getContext()
- if context.has_key('summary'):
+ if 'summary' in context:
self._summaryKey = 'content'
self._start_content(attrsD)
else:
@@ -1605,22 +1793,22 @@ class _FeedParserMixin:
def _start_enclosure(self, attrsD):
attrsD = self._itsAnHrefDamnIt(attrsD)
context = self._getContext()
- attrsD['rel']='enclosure'
+ attrsD['rel'] = 'enclosure'
context.setdefault('links', []).append(FeedParserDict(attrsD))
def _start_source(self, attrsD):
if 'url' in attrsD:
- # This means that we're processing a source element from an RSS 2.0 feed
- self.sourcedata['href'] = attrsD[u'url']
+ # This means that we're processing a source element from an RSS 2.0 feed
+ self.sourcedata['href'] = attrsD['url']
self.push('source', 1)
self.insource = 1
- self.hasTitle = 0
+ self.title_depth = -1
def _end_source(self):
self.insource = 0
value = self.pop('source')
if value:
- self.sourcedata['title'] = value
+ self.sourcedata['title'] = value
self._getContext()['source'] = copy.deepcopy(self.sourcedata)
self.sourcedata.clear()
@@ -1631,9 +1819,6 @@ class _FeedParserMixin:
self.contentparams['src'] = src
self.push('content', 1)
- def _start_prodlink(self, attrsD):
- self.pushContent('content', attrsD, 'text/html', 1)
-
def _start_body(self, attrsD):
self.pushContent('content', attrsD, 'application/xhtml+xml', 1)
_start_xhtml_body = _start_body
@@ -1652,12 +1837,13 @@ class _FeedParserMixin:
_end_xhtml_body = _end_content
_end_content_encoded = _end_content
_end_fullitem = _end_content
- _end_prodlink = _end_content
def _start_itunes_image(self, attrsD):
self.push('itunes_image', 0)
if attrsD.get('href'):
self._getContext()['image'] = FeedParserDict({'href': attrsD.get('href')})
+ elif attrsD.get('url'):
+ self._getContext()['image'] = FeedParserDict({'href': attrsD.get('url')})
_start_itunes_link = _start_itunes_image
def _end_itunes_block(self):
@@ -1671,6 +1857,55 @@ class _FeedParserMixin:
# by applications that only need to know if the content is explicit.
self._getContext()['itunes_explicit'] = (None, False, True)[(value == 'yes' and 2) or value == 'clean' or 0]
+ def _start_media_group(self, attrsD):
+ # don't do anything, but don't break the enclosed tags either
+ pass
+
+ def _start_media_rating(self, attrsD):
+ context = self._getContext()
+ context.setdefault('media_rating', attrsD)
+ self.push('rating', 1)
+
+ def _end_media_rating(self):
+ rating = self.pop('rating')
+ if rating is not None and rating.strip():
+ context = self._getContext()
+ context['media_rating']['content'] = rating
+
+ def _start_media_credit(self, attrsD):
+ context = self._getContext()
+ context.setdefault('media_credit', [])
+ context['media_credit'].append(attrsD)
+ self.push('credit', 1)
+
+ def _end_media_credit(self):
+ credit = self.pop('credit')
+ if credit != None and len(credit.strip()) != 0:
+ context = self._getContext()
+ context['media_credit'][-1]['content'] = credit
+
+ def _start_media_restriction(self, attrsD):
+ context = self._getContext()
+ context.setdefault('media_restriction', attrsD)
+ self.push('restriction', 1)
+
+ def _end_media_restriction(self):
+ restriction = self.pop('restriction')
+ if restriction != None and len(restriction.strip()) != 0:
+ context = self._getContext()
+ context['media_restriction']['content'] = [cc.strip().lower() for cc in restriction.split(' ')]
+
+ def _start_media_license(self, attrsD):
+ context = self._getContext()
+ context.setdefault('media_license', attrsD)
+ self.push('license', 1)
+
+ def _end_media_license(self):
+ license = self.pop('license')
+ if license != None and len(license.strip()) != 0:
+ context = self._getContext()
+ context['media_license']['content'] = license
+
def _start_media_content(self, attrsD):
context = self._getContext()
context.setdefault('media_content', [])
@@ -1686,7 +1921,7 @@ class _FeedParserMixin:
url = self.pop('url')
context = self._getContext()
if url != None and len(url.strip()) != 0:
- if not context['media_thumbnail'][-1].has_key('url'):
+ if 'url' not in context['media_thumbnail'][-1]:
context['media_thumbnail'][-1]['url'] = url
def _start_media_player(self, attrsD):
@@ -1709,10 +1944,29 @@ class _FeedParserMixin:
return
context['newlocation'] = _makeSafeAbsoluteURI(self.baseuri, url.strip())
+ def _start_psc_chapters(self, attrsD):
+ if self.psc_chapters_flag is None:
+ # Transition from None -> True
+ self.psc_chapters_flag = True
+ attrsD['chapters'] = []
+ self._getContext()['psc_chapters'] = FeedParserDict(attrsD)
+
+ def _end_psc_chapters(self):
+ # Transition from True -> False
+ self.psc_chapters_flag = False
+
+ def _start_psc_chapter(self, attrsD):
+ if self.psc_chapters_flag:
+ start = self._getAttribute(attrsD, 'start')
+ attrsD['start_parsed'] = _parse_psc_chapter_start(start)
+
+ context = self._getContext()['psc_chapters']
+ context['chapters'].append(FeedParserDict(attrsD))
+
+
if _XML_AVAILABLE:
class _StrictFeedParser(_FeedParserMixin, xml.sax.handler.ContentHandler):
def __init__(self, baseuri, baselang, encoding):
- if _debug: sys.stderr.write('trying StrictFeedParser\n')
xml.sax.handler.ContentHandler.__init__(self)
_FeedParserMixin.__init__(self, baseuri, baselang, encoding)
self.bozo = 0
@@ -1720,14 +1974,18 @@ if _XML_AVAILABLE:
self.decls = {}
def startPrefixMapping(self, prefix, uri):
+ if not uri:
+ return
+ # Jython uses '' instead of None; standardize on None
+ prefix = prefix or None
self.trackNamespace(prefix, uri)
- if uri == 'http://www.w3.org/1999/xlink':
- self.decls['xmlns:'+prefix] = uri
+ if prefix and uri == 'http://www.w3.org/1999/xlink':
+ self.decls['xmlns:' + prefix] = uri
def startElementNS(self, name, qname, attrs):
namespace, localname = name
lowernamespace = str(namespace or '').lower()
- if lowernamespace.find('backend.userland.com/rss') <> -1:
+ if lowernamespace.find('backend.userland.com/rss') != -1:
# match any backend.userland.com namespace
namespace = 'http://backend.userland.com/rss'
lowernamespace = namespace
@@ -1736,8 +1994,8 @@ if _XML_AVAILABLE:
else:
givenprefix = None
prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
- if givenprefix and (prefix is None or (prefix == '' and lowernamespace == '')) and not self.namespacesInUse.has_key(givenprefix):
- raise UndeclaredNamespace, "'%s' is not associated with a namespace" % givenprefix
+ if givenprefix and (prefix == None or (prefix == '' and lowernamespace == '')) and givenprefix not in self.namespacesInUse:
+ raise UndeclaredNamespace("'%s' is not associated with a namespace" % givenprefix)
localname = str(localname).lower()
# qname implementation is horribly broken in Python 2.1 (it
@@ -1756,13 +2014,12 @@ if _XML_AVAILABLE:
if prefix:
localname = prefix.lower() + ':' + localname
elif namespace and not qname: #Expat
- for name,value in self.namespacesInUse.items():
- if name and value == namespace:
- localname = name + ':' + localname
- break
- if _debug: sys.stderr.write('startElementNS: qname = %s, namespace = %s, givenprefix = %s, prefix = %s, attrs = %s, localname = %s\n' % (qname, namespace, givenprefix, prefix, attrs.items(), localname))
+ for name,value in list(self.namespacesInUse.items()):
+ if name and value == namespace:
+ localname = name + ':' + localname
+ break
- for (namespace, attrlocalname), attrvalue in attrs._attrs.items():
+ for (namespace, attrlocalname), attrvalue in list(attrs.items()):
lowernamespace = (namespace or '').lower()
prefix = self._matchnamespaces.get(lowernamespace, '')
if prefix:
@@ -1770,7 +2027,8 @@ if _XML_AVAILABLE:
attrsD[str(attrlocalname).lower()] = attrvalue
for qname in attrs.getQNames():
attrsD[str(qname).lower()] = attrs.getValueByQName(qname)
- self.unknown_starttag(localname, attrsD.items())
+ localname = str(localname).lower()
+ self.unknown_starttag(localname, list(attrsD.items()))
def characters(self, text):
self.handle_data(text)
@@ -1786,10 +2044,10 @@ if _XML_AVAILABLE:
if prefix:
localname = prefix + ':' + localname
elif namespace and not qname: #Expat
- for name,value in self.namespacesInUse.items():
- if name and value == namespace:
- localname = name + ':' + localname
- break
+ for name,value in list(self.namespacesInUse.items()):
+ if name and value == namespace:
+ localname = name + ':' + localname
+ break
localname = str(localname).lower()
self.unknown_endtag(localname)
@@ -1797,6 +2055,9 @@ if _XML_AVAILABLE:
self.bozo = 1
self.exc = exc
+ # drv_libxml2 calls warning() in some cases
+ warning = error
+
def fatalError(self, exc):
self.error(exc)
raise exc
@@ -1804,16 +2065,15 @@ if _XML_AVAILABLE:
class _BaseHTMLProcessor(sgmllib.SGMLParser):
special = re.compile('''[<>'"]''')
bare_ampersand = re.compile("&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)")
- elements_no_end_tag = [
+ elements_no_end_tag = set([
'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame',
'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param',
'source', 'track', 'wbr'
- ]
+ ])
def __init__(self, encoding, _type):
self.encoding = encoding
self._type = _type
- if _debug: sys.stderr.write('entering BaseHTMLProcessor, encoding=%s\n' % self.encoding)
sgmllib.SGMLParser.__init__(self)
def reset(self):
@@ -1827,8 +2087,21 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
else:
return '<' + tag + '>' + tag + '>'
+ # By declaring these methods and overriding their compiled code
+ # with the code from sgmllib, the original code will execute in
+ # feedparser's scope instead of sgmllib's. This means that the
+ # `tagfind` and `charref` regular expressions will be found as
+ # they're declared above, not as they're declared in sgmllib.
+ def goahead(self, i):
+ pass
+ goahead.__code__ = sgmllib.SGMLParser.goahead.__code__
+
+ def __parse_starttag(self, i):
+ pass
+ __parse_starttag.__code__ = sgmllib.SGMLParser.parse_starttag.__code__
+
def parse_starttag(self,i):
- j=sgmllib.SGMLParser.parse_starttag(self, i)
+ j = self.__parse_starttag(i)
if self._type == 'application/xhtml+xml':
if j>2 and self.rawdata[j-2:j]=='/>':
self.unknown_endtag(self.lasttag)
@@ -1836,7 +2109,6 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
def feed(self, data):
data = re.compile(r'', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace
data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data)
data = data.replace(''', "'")
data = data.replace('"', '"')
@@ -1846,15 +2118,16 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
raise NameError
self.encoding = self.encoding + '_INVALID_PYTHON_3'
except NameError:
- if self.encoding and type(data) == type(u''):
+ if self.encoding and isinstance(data, str):
data = data.encode(self.encoding)
sgmllib.SGMLParser.feed(self, data)
sgmllib.SGMLParser.close(self)
def normalize_attrs(self, attrs):
- if not attrs: return attrs
+ if not attrs:
+ return attrs
# utility method to be called by descendants
- attrs = dict([(k.lower(), v) for k, v in attrs]).items()
+ attrs = list(dict([(k.lower(), v) for k, v in attrs]).items())
attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs]
attrs.sort()
return attrs
@@ -1863,7 +2136,6 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
# called for each start tag
# attrs is a list of (attr, value) tuples
# e.g. for
, tag='pre', attrs=[('class', 'screen')]
- if _debug: sys.stderr.write('_BaseHTMLProcessor, unknown_starttag, tag=%s\n' % tag)
uattrs = []
strattrs=''
if attrs:
@@ -1871,77 +2143,74 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
value=value.replace('>','>').replace('<','<').replace('"','"')
value = self.bare_ampersand.sub("&", value)
# thanks to Kevin Marks for this breathtaking hack to deal with (valid) high-bit attribute values in UTF-8 feeds
- if type(value) != type(u''):
- try:
- value = unicode(value, self.encoding)
- except:
- value = unicode(value, 'iso-8859-1')
+ if not isinstance(value, str):
+ value = value.decode(self.encoding, 'ignore')
try:
# Currently, in Python 3 the key is already a str, and cannot be decoded again
- uattrs.append((unicode(key, self.encoding), value))
+ uattrs.append((str(key, self.encoding), value))
except TypeError:
uattrs.append((key, value))
- strattrs = u''.join([u' %s="%s"' % (key, value) for key, value in uattrs])
+ strattrs = ''.join([' %s="%s"' % (key, value) for key, value in uattrs])
if self.encoding:
try:
- strattrs=strattrs.encode(self.encoding)
- except:
+ strattrs = strattrs.encode(self.encoding)
+ except (UnicodeEncodeError, LookupError):
pass
if tag in self.elements_no_end_tag:
- self.pieces.append('<%(tag)s%(strattrs)s />' % locals())
+ self.pieces.append('<%s%s />' % (tag, strattrs))
else:
- self.pieces.append('<%(tag)s%(strattrs)s>' % locals())
+ self.pieces.append('<%s%s>' % (tag, strattrs))
def unknown_endtag(self, tag):
# called for each end tag, e.g. for
, tag will be 'pre'
# Reconstruct the original end tag.
if tag not in self.elements_no_end_tag:
- self.pieces.append("%(tag)s>" % locals())
+ self.pieces.append("%s>" % tag)
def handle_charref(self, ref):
# called for each character reference, e.g. for ' ', ref will be '160'
# Reconstruct the original character reference.
+ ref = ref.lower()
if ref.startswith('x'):
- value = unichr(int(ref[1:],16))
+ value = int(ref[1:], 16)
else:
- value = unichr(int(ref))
+ value = int(ref)
- if value in _cp1252.keys():
+ if value in _cp1252:
self.pieces.append('%s;' % hex(ord(_cp1252[value]))[1:])
else:
- self.pieces.append('%(ref)s;' % locals())
+ self.pieces.append('%s;' % ref)
def handle_entityref(self, ref):
# called for each entity reference, e.g. for '©', ref will be 'copy'
# Reconstruct the original entity reference.
- if name2codepoint.has_key(ref):
- self.pieces.append('&%(ref)s;' % locals())
+ if ref in name2codepoint or ref == 'apos':
+ self.pieces.append('&%s;' % ref)
else:
- self.pieces.append('&%(ref)s' % locals())
+ self.pieces.append('&%s' % ref)
def handle_data(self, text):
# called for each block of plain text, i.e. outside of any tag and
# not containing any character or entity references
# Store the original text verbatim.
- if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_data, text=%s\n' % text)
self.pieces.append(text)
def handle_comment(self, text):
# called for each HTML comment, e.g.
# Reconstruct the original comment.
- self.pieces.append('' % locals())
+ self.pieces.append('' % text)
def handle_pi(self, text):
# called for each processing instruction, e.g.
# Reconstruct original processing instruction.
- self.pieces.append('%(text)s>' % locals())
+ self.pieces.append('%s>' % text)
def handle_decl(self, text):
# called for the DOCTYPE, if present, e.g.
#
# Reconstruct original DOCTYPE
- self.pieces.append('' % locals())
+ self.pieces.append('' % text)
_new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
def _scan_name(self, i, declstartpos):
@@ -1999,439 +2268,24 @@ class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor):
data = data.replace('"', '"')
data = data.replace(''', ''')
data = data.replace(''', ''')
- if self.contentparams.has_key('type') and not self.contentparams.get('type', 'xml').endswith('xml'):
+ if not self.contentparams.get('type', 'xml').endswith('xml'):
data = data.replace('<', '<')
data = data.replace('>', '>')
data = data.replace('&', '&')
data = data.replace('"', '"')
data = data.replace(''', "'")
+ data = data.replace('/', '/')
+ data = data.replace('/', '/')
return data
def strattrs(self, attrs):
return ''.join([' %s="%s"' % (n,v.replace('"','"')) for n,v in attrs])
-class _MicroformatsParser:
- STRING = 1
- DATE = 2
- URI = 3
- NODE = 4
- EMAIL = 5
-
- known_xfn_relationships = ['contact', 'acquaintance', 'friend', 'met', 'co-worker', 'coworker', 'colleague', 'co-resident', 'coresident', 'neighbor', 'child', 'parent', 'sibling', 'brother', 'sister', 'spouse', 'wife', 'husband', 'kin', 'relative', 'muse', 'crush', 'date', 'sweetheart', 'me']
- known_binary_extensions = ['zip','rar','exe','gz','tar','tgz','tbz2','bz2','z','7z','dmg','img','sit','sitx','hqx','deb','rpm','bz2','jar','rar','iso','bin','msi','mp2','mp3','ogg','ogm','mp4','m4v','m4a','avi','wma','wmv']
-
- def __init__(self, data, baseuri, encoding):
- self.document = BeautifulSoup.BeautifulSoup(data)
- self.baseuri = baseuri
- self.encoding = encoding
- if type(data) == type(u''):
- data = data.encode(encoding)
- self.tags = []
- self.enclosures = []
- self.xfn = []
- self.vcard = None
-
- def vcardEscape(self, s):
- if type(s) in (type(''), type(u'')):
- s = s.replace(',', '\\,').replace(';', '\\;').replace('\n', '\\n')
- return s
-
- def vcardFold(self, s):
- s = re.sub(';+$', '', s)
- sFolded = ''
- iMax = 75
- sPrefix = ''
- while len(s) > iMax:
- sFolded += sPrefix + s[:iMax] + '\n'
- s = s[iMax:]
- sPrefix = ' '
- iMax = 74
- sFolded += sPrefix + s
- return sFolded
-
- def normalize(self, s):
- return re.sub(r'\s+', ' ', s).strip()
-
- def unique(self, aList):
- results = []
- for element in aList:
- if element not in results:
- results.append(element)
- return results
-
- def toISO8601(self, dt):
- return time.strftime('%Y-%m-%dT%H:%M:%SZ', dt)
-
- def getPropertyValue(self, elmRoot, sProperty, iPropertyType=4, bAllowMultiple=0, bAutoEscape=0):
- all = lambda x: 1
- sProperty = sProperty.lower()
- bFound = 0
- bNormalize = 1
- propertyMatch = {'class': re.compile(r'\b%s\b' % sProperty)}
- if bAllowMultiple and (iPropertyType != self.NODE):
- snapResults = []
- containers = elmRoot(['ul', 'ol'], propertyMatch)
- for container in containers:
- snapResults.extend(container('li'))
- bFound = (len(snapResults) != 0)
- if not bFound:
- snapResults = elmRoot(all, propertyMatch)
- bFound = (len(snapResults) != 0)
- if (not bFound) and (sProperty == 'value'):
- snapResults = elmRoot('pre')
- bFound = (len(snapResults) != 0)
- bNormalize = not bFound
- if not bFound:
- snapResults = [elmRoot]
- bFound = (len(snapResults) != 0)
- arFilter = []
- if sProperty == 'vcard':
- snapFilter = elmRoot(all, propertyMatch)
- for node in snapFilter:
- if node.findParent(all, propertyMatch):
- arFilter.append(node)
- arResults = []
- for node in snapResults:
- if node not in arFilter:
- arResults.append(node)
- bFound = (len(arResults) != 0)
- if not bFound:
- if bAllowMultiple: return []
- elif iPropertyType == self.STRING: return ''
- elif iPropertyType == self.DATE: return None
- elif iPropertyType == self.URI: return ''
- elif iPropertyType == self.NODE: return None
- else: return None
- arValues = []
- for elmResult in arResults:
- sValue = None
- if iPropertyType == self.NODE:
- if bAllowMultiple:
- arValues.append(elmResult)
- continue
- else:
- return elmResult
- sNodeName = elmResult.name.lower()
- if (iPropertyType == self.EMAIL) and (sNodeName == 'a'):
- sValue = (elmResult.get('href') or '').split('mailto:').pop().split('?')[0]
- if sValue:
- sValue = bNormalize and self.normalize(sValue) or sValue.strip()
- if (not sValue) and (sNodeName == 'abbr'):
- sValue = elmResult.get('title')
- if sValue:
- sValue = bNormalize and self.normalize(sValue) or sValue.strip()
- if (not sValue) and (iPropertyType == self.URI):
- if sNodeName == 'a': sValue = elmResult.get('href')
- elif sNodeName == 'img': sValue = elmResult.get('src')
- elif sNodeName == 'object': sValue = elmResult.get('data')
- if sValue:
- sValue = bNormalize and self.normalize(sValue) or sValue.strip()
- if (not sValue) and (sNodeName == 'img'):
- sValue = elmResult.get('alt')
- if sValue:
- sValue = bNormalize and self.normalize(sValue) or sValue.strip()
- if not sValue:
- sValue = elmResult.renderContents()
- sValue = re.sub(r'<\S[^>]*>', '', sValue)
- sValue = sValue.replace('\r\n', '\n')
- sValue = sValue.replace('\r', '\n')
- if sValue:
- sValue = bNormalize and self.normalize(sValue) or sValue.strip()
- if not sValue: continue
- if iPropertyType == self.DATE:
- sValue = _parse_date_iso8601(sValue)
- if bAllowMultiple:
- arValues.append(bAutoEscape and self.vcardEscape(sValue) or sValue)
- else:
- return bAutoEscape and self.vcardEscape(sValue) or sValue
- return arValues
-
- def findVCards(self, elmRoot, bAgentParsing=0):
- sVCards = ''
-
- if not bAgentParsing:
- arCards = self.getPropertyValue(elmRoot, 'vcard', bAllowMultiple=1)
- else:
- arCards = [elmRoot]
-
- for elmCard in arCards:
- arLines = []
-
- def processSingleString(sProperty):
- sValue = self.getPropertyValue(elmCard, sProperty, self.STRING, bAutoEscape=1).decode(self.encoding)
- if sValue:
- arLines.append(self.vcardFold(sProperty.upper() + ':' + sValue))
- return sValue or u''
-
- def processSingleURI(sProperty):
- sValue = self.getPropertyValue(elmCard, sProperty, self.URI)
- if sValue:
- sContentType = ''
- sEncoding = ''
- sValueKey = ''
- if sValue.startswith('data:'):
- sEncoding = ';ENCODING=b'
- sContentType = sValue.split(';')[0].split('/').pop()
- sValue = sValue.split(',', 1).pop()
- else:
- elmValue = self.getPropertyValue(elmCard, sProperty)
- if elmValue:
- if sProperty != 'url':
- sValueKey = ';VALUE=uri'
- sContentType = elmValue.get('type', '').strip().split('/').pop().strip()
- sContentType = sContentType.upper()
- if sContentType == 'OCTET-STREAM':
- sContentType = ''
- if sContentType:
- sContentType = ';TYPE=' + sContentType.upper()
- arLines.append(self.vcardFold(sProperty.upper() + sEncoding + sContentType + sValueKey + ':' + sValue))
-
- def processTypeValue(sProperty, arDefaultType, arForceType=None):
- arResults = self.getPropertyValue(elmCard, sProperty, bAllowMultiple=1)
- for elmResult in arResults:
- arType = self.getPropertyValue(elmResult, 'type', self.STRING, 1, 1)
- if arForceType:
- arType = self.unique(arForceType + arType)
- if not arType:
- arType = arDefaultType
- sValue = self.getPropertyValue(elmResult, 'value', self.EMAIL, 0)
- if sValue:
- arLines.append(self.vcardFold(sProperty.upper() + ';TYPE=' + ','.join(arType) + ':' + sValue))
-
- # AGENT
- # must do this before all other properties because it is destructive
- # (removes nested class="vcard" nodes so they don't interfere with
- # this vcard's other properties)
- arAgent = self.getPropertyValue(elmCard, 'agent', bAllowMultiple=1)
- for elmAgent in arAgent:
- if re.compile(r'\bvcard\b').search(elmAgent.get('class')):
- sAgentValue = self.findVCards(elmAgent, 1) + '\n'
- sAgentValue = sAgentValue.replace('\n', '\\n')
- sAgentValue = sAgentValue.replace(';', '\\;')
- if sAgentValue:
- arLines.append(self.vcardFold('AGENT:' + sAgentValue))
- # Completely remove the agent element from the parse tree
- elmAgent.extract()
- else:
- sAgentValue = self.getPropertyValue(elmAgent, 'value', self.URI, bAutoEscape=1);
- if sAgentValue:
- arLines.append(self.vcardFold('AGENT;VALUE=uri:' + sAgentValue))
-
- # FN (full name)
- sFN = processSingleString('fn')
-
- # N (name)
- elmName = self.getPropertyValue(elmCard, 'n')
- if elmName:
- sFamilyName = self.getPropertyValue(elmName, 'family-name', self.STRING, bAutoEscape=1)
- sGivenName = self.getPropertyValue(elmName, 'given-name', self.STRING, bAutoEscape=1)
- arAdditionalNames = self.getPropertyValue(elmName, 'additional-name', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'additional-names', self.STRING, 1, 1)
- arHonorificPrefixes = self.getPropertyValue(elmName, 'honorific-prefix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-prefixes', self.STRING, 1, 1)
- arHonorificSuffixes = self.getPropertyValue(elmName, 'honorific-suffix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-suffixes', self.STRING, 1, 1)
- arLines.append(self.vcardFold('N:' + sFamilyName + ';' +
- sGivenName + ';' +
- ','.join(arAdditionalNames) + ';' +
- ','.join(arHonorificPrefixes) + ';' +
- ','.join(arHonorificSuffixes)))
- elif sFN:
- # implied "N" optimization
- # http://microformats.org/wiki/hcard#Implied_.22N.22_Optimization
- arNames = self.normalize(sFN).split()
- if len(arNames) == 2:
- bFamilyNameFirst = (arNames[0].endswith(',') or
- len(arNames[1]) == 1 or
- ((len(arNames[1]) == 2) and (arNames[1].endswith('.'))))
- if bFamilyNameFirst:
- arLines.append(self.vcardFold('N:' + arNames[0] + ';' + arNames[1]))
- else:
- arLines.append(self.vcardFold('N:' + arNames[1] + ';' + arNames[0]))
-
- # SORT-STRING
- sSortString = self.getPropertyValue(elmCard, 'sort-string', self.STRING, bAutoEscape=1)
- if sSortString:
- arLines.append(self.vcardFold('SORT-STRING:' + sSortString))
-
- # NICKNAME
- arNickname = self.getPropertyValue(elmCard, 'nickname', self.STRING, 1, 1)
- if arNickname:
- arLines.append(self.vcardFold('NICKNAME:' + ','.join(arNickname)))
-
- # PHOTO
- processSingleURI('photo')
-
- # BDAY
- dtBday = self.getPropertyValue(elmCard, 'bday', self.DATE)
- if dtBday:
- arLines.append(self.vcardFold('BDAY:' + self.toISO8601(dtBday)))
-
- # ADR (address)
- arAdr = self.getPropertyValue(elmCard, 'adr', bAllowMultiple=1)
- for elmAdr in arAdr:
- arType = self.getPropertyValue(elmAdr, 'type', self.STRING, 1, 1)
- if not arType:
- arType = ['intl','postal','parcel','work'] # default adr types, see RFC 2426 section 3.2.1
- sPostOfficeBox = self.getPropertyValue(elmAdr, 'post-office-box', self.STRING, 0, 1)
- sExtendedAddress = self.getPropertyValue(elmAdr, 'extended-address', self.STRING, 0, 1)
- sStreetAddress = self.getPropertyValue(elmAdr, 'street-address', self.STRING, 0, 1)
- sLocality = self.getPropertyValue(elmAdr, 'locality', self.STRING, 0, 1)
- sRegion = self.getPropertyValue(elmAdr, 'region', self.STRING, 0, 1)
- sPostalCode = self.getPropertyValue(elmAdr, 'postal-code', self.STRING, 0, 1)
- sCountryName = self.getPropertyValue(elmAdr, 'country-name', self.STRING, 0, 1)
- arLines.append(self.vcardFold('ADR;TYPE=' + ','.join(arType) + ':' +
- sPostOfficeBox + ';' +
- sExtendedAddress + ';' +
- sStreetAddress + ';' +
- sLocality + ';' +
- sRegion + ';' +
- sPostalCode + ';' +
- sCountryName))
-
- # LABEL
- processTypeValue('label', ['intl','postal','parcel','work'])
-
- # TEL (phone number)
- processTypeValue('tel', ['voice'])
-
- # EMAIL
- processTypeValue('email', ['internet'], ['internet'])
-
- # MAILER
- processSingleString('mailer')
-
- # TZ (timezone)
- processSingleString('tz')
-
- # GEO (geographical information)
- elmGeo = self.getPropertyValue(elmCard, 'geo')
- if elmGeo:
- sLatitude = self.getPropertyValue(elmGeo, 'latitude', self.STRING, 0, 1)
- sLongitude = self.getPropertyValue(elmGeo, 'longitude', self.STRING, 0, 1)
- arLines.append(self.vcardFold('GEO:' + sLatitude + ';' + sLongitude))
-
- # TITLE
- processSingleString('title')
-
- # ROLE
- processSingleString('role')
-
- # LOGO
- processSingleURI('logo')
-
- # ORG (organization)
- elmOrg = self.getPropertyValue(elmCard, 'org')
- if elmOrg:
- sOrganizationName = self.getPropertyValue(elmOrg, 'organization-name', self.STRING, 0, 1)
- if not sOrganizationName:
- # implied "organization-name" optimization
- # http://microformats.org/wiki/hcard#Implied_.22organization-name.22_Optimization
- sOrganizationName = self.getPropertyValue(elmCard, 'org', self.STRING, 0, 1)
- if sOrganizationName:
- arLines.append(self.vcardFold('ORG:' + sOrganizationName))
- else:
- arOrganizationUnit = self.getPropertyValue(elmOrg, 'organization-unit', self.STRING, 1, 1)
- arLines.append(self.vcardFold('ORG:' + sOrganizationName + ';' + ';'.join(arOrganizationUnit)))
-
- # CATEGORY
- arCategory = self.getPropertyValue(elmCard, 'category', self.STRING, 1, 1) + self.getPropertyValue(elmCard, 'categories', self.STRING, 1, 1)
- if arCategory:
- arLines.append(self.vcardFold('CATEGORIES:' + ','.join(arCategory)))
-
- # NOTE
- processSingleString('note')
-
- # REV
- processSingleString('rev')
-
- # SOUND
- processSingleURI('sound')
-
- # UID
- processSingleString('uid')
-
- # URL
- processSingleURI('url')
-
- # CLASS
- processSingleString('class')
-
- # KEY
- processSingleURI('key')
-
- if arLines:
- arLines = [u'BEGIN:vCard',u'VERSION:3.0'] + arLines + [u'END:vCard']
- sVCards += u'\n'.join(arLines) + u'\n'
-
- return sVCards.strip()
-
- def isProbablyDownloadable(self, elm):
- attrsD = elm.attrMap
- if not attrsD.has_key('href'): return 0
- linktype = attrsD.get('type', '').strip()
- if linktype.startswith('audio/') or \
- linktype.startswith('video/') or \
- (linktype.startswith('application/') and not linktype.endswith('xml')):
- return 1
- path = urlparse.urlparse(attrsD['href'])[2]
- if path.find('.') == -1: return 0
- fileext = path.split('.').pop().lower()
- return fileext in self.known_binary_extensions
-
- def findTags(self):
- all = lambda x: 1
- for elm in self.document(all, {'rel': re.compile(r'\btag\b')}):
- href = elm.get('href')
- if not href: continue
- urlscheme, domain, path, params, query, fragment = \
- urlparse.urlparse(_urljoin(self.baseuri, href))
- segments = path.split('/')
- tag = segments.pop()
- if not tag:
- tag = segments.pop()
- tagscheme = urlparse.urlunparse((urlscheme, domain, '/'.join(segments), '', '', ''))
- if not tagscheme.endswith('/'):
- tagscheme += '/'
- self.tags.append(FeedParserDict({"term": tag, "scheme": tagscheme, "label": elm.string or ''}))
-
- def findEnclosures(self):
- all = lambda x: 1
- enclosure_match = re.compile(r'\benclosure\b')
- for elm in self.document(all, {'href': re.compile(r'.+')}):
- if not enclosure_match.search(elm.get('rel', '')) and not self.isProbablyDownloadable(elm): continue
- if elm.attrMap not in self.enclosures:
- self.enclosures.append(elm.attrMap)
- if elm.string and not elm.get('title'):
- self.enclosures[-1]['title'] = elm.string
-
- def findXFN(self):
- all = lambda x: 1
- for elm in self.document(all, {'rel': re.compile('.+'), 'href': re.compile('.+')}):
- rels = elm.get('rel', '').split()
- xfn_rels = []
- for rel in rels:
- if rel in self.known_xfn_relationships:
- xfn_rels.append(rel)
- if xfn_rels:
- self.xfn.append({"relationships": xfn_rels, "href": elm.get('href', ''), "name": elm.string})
-
-def _parseMicroformats(htmlSource, baseURI, encoding):
- if not BeautifulSoup: return
- if _debug: sys.stderr.write('entering _parseMicroformats\n')
- try:
- p = _MicroformatsParser(htmlSource, baseURI, encoding)
- except UnicodeEncodeError:
- # sgmllib throws this exception when performing lookups of tags
- # with non-ASCII characters in them.
- return
- p.vcard = p.findVCards(p.document)
- p.findTags()
- p.findEnclosures()
- p.findXFN()
- return {"tags": p.tags, "enclosures": p.enclosures, "xfn": p.xfn, "vcard": p.vcard}
-
class _RelativeURIResolver(_BaseHTMLProcessor):
- relative_uris = [('a', 'href'),
+ relative_uris = set([('a', 'href'),
('applet', 'codebase'),
('area', 'href'),
+ ('audio', 'src'),
('blockquote', 'cite'),
('body', 'background'),
('del', 'cite'),
@@ -2453,25 +2307,26 @@ class _RelativeURIResolver(_BaseHTMLProcessor):
('object', 'data'),
('object', 'usemap'),
('q', 'cite'),
- ('script', 'src')]
+ ('script', 'src'),
+ ('source', 'src'),
+ ('video', 'poster'),
+ ('video', 'src')])
def __init__(self, baseuri, encoding, _type):
_BaseHTMLProcessor.__init__(self, encoding, _type)
self.baseuri = baseuri
def resolveURI(self, uri):
- return _makeSafeAbsoluteURI(_urljoin(self.baseuri, uri.strip()))
+ return _makeSafeAbsoluteURI(self.baseuri, uri.strip())
def unknown_starttag(self, tag, attrs):
- if _debug:
- sys.stderr.write('tag: [%s] with attributes: [%s]\n' % (tag, str(attrs)))
attrs = self.normalize_attrs(attrs)
attrs = [(key, ((tag, key) in self.relative_uris) and self.resolveURI(value) or value) for key, value in attrs]
_BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
def _resolveRelativeURIs(htmlSource, baseURI, encoding, _type):
- if _debug:
- sys.stderr.write('entering _resolveRelativeURIs\n')
+ if not _SGML_AVAILABLE:
+ return htmlSource
p = _RelativeURIResolver(baseURI, encoding, _type)
p.feed(htmlSource)
@@ -2480,21 +2335,24 @@ def _resolveRelativeURIs(htmlSource, baseURI, encoding, _type):
def _makeSafeAbsoluteURI(base, rel=None):
# bail if ACCEPTABLE_URI_SCHEMES is empty
if not ACCEPTABLE_URI_SCHEMES:
- return _urljoin(base, rel or u'')
+ return _urljoin(base, rel or '')
if not base:
- return rel or u''
+ return rel or ''
if not rel:
- scheme = urlparse.urlparse(base)[0]
+ try:
+ scheme = urllib.parse.urlparse(base)[0]
+ except ValueError:
+ return ''
if not scheme or scheme in ACCEPTABLE_URI_SCHEMES:
return base
- return u''
+ return ''
uri = _urljoin(base, rel)
if uri.strip().split(':', 1)[0] not in ACCEPTABLE_URI_SCHEMES:
- return u''
+ return ''
return uri
class _HTMLSanitizer(_BaseHTMLProcessor):
- acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area',
+ acceptable_elements = set(['a', 'abbr', 'acronym', 'address', 'area',
'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button',
'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn',
@@ -2506,9 +2364,9 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select',
'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong',
'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot',
- 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video', 'noscript']
+ 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video', 'noscript'])
- acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey',
+ acceptable_attributes = set(['abbr', 'accept', 'accept-charset', 'accesskey',
'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis',
'background', 'balance', 'bgcolor', 'bgproperties', 'border',
'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding',
@@ -2523,17 +2381,17 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
'loop', 'loopcount', 'loopend', 'loopstart', 'low', 'lowsrc', 'max',
'maxlength', 'media', 'method', 'min', 'multiple', 'name', 'nohref',
'noshade', 'nowrap', 'open', 'optimum', 'pattern', 'ping', 'point-size',
- 'prompt', 'pqg', 'radiogroup', 'readonly', 'rel', 'repeat-max',
- 'repeat-min', 'replace', 'required', 'rev', 'rightspacing', 'rows',
- 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src',
- 'start', 'step', 'summary', 'suppress', 'tabindex', 'target', 'template',
- 'title', 'toppadding', 'type', 'unselectable', 'usemap', 'urn', 'valign',
- 'value', 'variable', 'volume', 'vspace', 'vrml', 'width', 'wrap',
- 'xml:lang']
+ 'poster', 'pqg', 'preload', 'prompt', 'radiogroup', 'readonly', 'rel',
+ 'repeat-max', 'repeat-min', 'replace', 'required', 'rev', 'rightspacing',
+ 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size', 'span',
+ 'src', 'start', 'step', 'summary', 'suppress', 'tabindex', 'target',
+ 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap',
+ 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml',
+ 'width', 'wrap', 'xml:lang'])
- unacceptable_elements_with_end_tag = ['script', 'applet', 'style']
+ unacceptable_elements_with_end_tag = set(['script', 'applet', 'style'])
- acceptable_css_properties = ['azimuth', 'background-color',
+ acceptable_css_properties = set(['azimuth', 'background-color',
'border-bottom-color', 'border-collapse', 'border-color',
'border-left-color', 'border-right-color', 'border-top-color', 'clear',
'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font',
@@ -2543,45 +2401,178 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
'speak', 'speak-header', 'speak-numeral', 'speak-punctuation',
'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent',
'unicode-bidi', 'vertical-align', 'voice-family', 'volume',
- 'white-space', 'width']
+ 'white-space', 'width'])
# survey of common keywords found in feeds
- acceptable_css_keywords = ['auto', 'aqua', 'black', 'block', 'blue',
+ acceptable_css_keywords = set(['auto', 'aqua', 'black', 'block', 'blue',
'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed',
'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left',
'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive',
'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top',
- 'transparent', 'underline', 'white', 'yellow']
+ 'transparent', 'underline', 'white', 'yellow'])
valid_css_values = re.compile('^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|' +
'\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$')
- mathml_elements = ['annotation', 'annotation-xml', 'maction', 'math',
- 'merror', 'mfenced', 'mfrac', 'mi', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded',
- 'mphantom', 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle',
- 'msub', 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder',
- 'munderover', 'none', 'semantics']
+ mathml_elements = set([
+ 'annotation',
+ 'annotation-xml',
+ 'maction',
+ 'maligngroup',
+ 'malignmark',
+ 'math',
+ 'menclose',
+ 'merror',
+ 'mfenced',
+ 'mfrac',
+ 'mglyph',
+ 'mi',
+ 'mlabeledtr',
+ 'mlongdiv',
+ 'mmultiscripts',
+ 'mn',
+ 'mo',
+ 'mover',
+ 'mpadded',
+ 'mphantom',
+ 'mprescripts',
+ 'mroot',
+ 'mrow',
+ 'ms',
+ 'mscarries',
+ 'mscarry',
+ 'msgroup',
+ 'msline',
+ 'mspace',
+ 'msqrt',
+ 'msrow',
+ 'mstack',
+ 'mstyle',
+ 'msub',
+ 'msubsup',
+ 'msup',
+ 'mtable',
+ 'mtd',
+ 'mtext',
+ 'mtr',
+ 'munder',
+ 'munderover',
+ 'none',
+ 'semantics',
+ ])
- mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign',
- 'columnalign', 'close', 'columnlines', 'columnspacing', 'columnspan', 'depth',
- 'display', 'displaystyle', 'encoding', 'equalcolumns', 'equalrows',
- 'fence', 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness',
- 'lspace', 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant',
- 'maxsize', 'minsize', 'open', 'other', 'rowalign', 'rowalign', 'rowalign',
- 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection',
- 'separator', 'separators', 'stretchy', 'width', 'width', 'xlink:href',
- 'xlink:show', 'xlink:type', 'xmlns', 'xmlns:xlink']
+ mathml_attributes = set([
+ 'accent',
+ 'accentunder',
+ 'actiontype',
+ 'align',
+ 'alignmentscope',
+ 'altimg',
+ 'altimg-height',
+ 'altimg-valign',
+ 'altimg-width',
+ 'alttext',
+ 'bevelled',
+ 'charalign',
+ 'close',
+ 'columnalign',
+ 'columnlines',
+ 'columnspacing',
+ 'columnspan',
+ 'columnwidth',
+ 'crossout',
+ 'decimalpoint',
+ 'denomalign',
+ 'depth',
+ 'dir',
+ 'display',
+ 'displaystyle',
+ 'edge',
+ 'encoding',
+ 'equalcolumns',
+ 'equalrows',
+ 'fence',
+ 'fontstyle',
+ 'fontweight',
+ 'form',
+ 'frame',
+ 'framespacing',
+ 'groupalign',
+ 'height',
+ 'href',
+ 'id',
+ 'indentalign',
+ 'indentalignfirst',
+ 'indentalignlast',
+ 'indentshift',
+ 'indentshiftfirst',
+ 'indentshiftlast',
+ 'indenttarget',
+ 'infixlinebreakstyle',
+ 'largeop',
+ 'length',
+ 'linebreak',
+ 'linebreakmultchar',
+ 'linebreakstyle',
+ 'lineleading',
+ 'linethickness',
+ 'location',
+ 'longdivstyle',
+ 'lquote',
+ 'lspace',
+ 'mathbackground',
+ 'mathcolor',
+ 'mathsize',
+ 'mathvariant',
+ 'maxsize',
+ 'minlabelspacing',
+ 'minsize',
+ 'movablelimits',
+ 'notation',
+ 'numalign',
+ 'open',
+ 'other',
+ 'overflow',
+ 'position',
+ 'rowalign',
+ 'rowlines',
+ 'rowspacing',
+ 'rowspan',
+ 'rquote',
+ 'rspace',
+ 'scriptlevel',
+ 'scriptminsize',
+ 'scriptsizemultiplier',
+ 'selection',
+ 'separator',
+ 'separators',
+ 'shift',
+ 'side',
+ 'src',
+ 'stackalign',
+ 'stretchy',
+ 'subscriptshift',
+ 'superscriptshift',
+ 'symmetric',
+ 'voffset',
+ 'width',
+ 'xlink:href',
+ 'xlink:show',
+ 'xlink:type',
+ 'xmlns',
+ 'xmlns:xlink',
+ ])
# svgtiny - foreignObject + linearGradient + radialGradient + stop
- svg_elements = ['a', 'animate', 'animateColor', 'animateMotion',
+ svg_elements = set(['a', 'animate', 'animateColor', 'animateMotion',
'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'foreignObject',
'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern',
'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', 'mpath',
'path', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop',
- 'svg', 'switch', 'text', 'title', 'tspan', 'use']
+ 'svg', 'switch', 'text', 'title', 'tspan', 'use'])
# svgtiny + class + opacity + offset + xmlns + xmlns:xlink
- svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic',
+ svg_attributes = set(['accent-height', 'accumulate', 'additive', 'alphabetic',
'arabic-form', 'ascent', 'attributeName', 'attributeType',
'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height',
'class', 'color', 'color-rendering', 'content', 'cx', 'cy', 'd', 'dx',
@@ -2607,14 +2598,14 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
'widths', 'x', 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole',
'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type',
'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', 'y1',
- 'y2', 'zoomAndPan']
+ 'y2', 'zoomAndPan'])
svg_attr_map = None
svg_elem_map = None
- acceptable_svg_properties = [ 'fill', 'fill-opacity', 'fill-rule',
+ acceptable_svg_properties = set([ 'fill', 'fill-opacity', 'fill-rule',
'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
- 'stroke-opacity']
+ 'stroke-opacity'])
def reset(self):
_BaseHTMLProcessor.reset(self)
@@ -2667,7 +2658,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
# declare xlink namespace, if needed
if self.mathmlOK or self.svgOK:
- if filter(lambda (n,v): n.startswith('xlink:'),attrs):
+ if [n_v for n_v in attrs if n_v[0].startswith('xlink:')]:
if not ('xmlns:xlink','http://www.w3.org/1999/xlink') in attrs:
attrs.append(('xmlns:xlink','http://www.w3.org/1999/xlink'))
@@ -2676,12 +2667,13 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
if key in acceptable_attributes:
key=keymap.get(key,key)
# make sure the uri uses an acceptable uri scheme
- if key == u'href':
+ if key == 'href':
value = _makeSafeAbsoluteURI(value)
clean_attrs.append((key,value))
elif key=='style':
clean_value = self.sanitize_style(value)
- if clean_value: clean_attrs.append((key,clean_value))
+ if clean_value:
+ clean_attrs.append((key,clean_value))
_BaseHTMLProcessor.unknown_starttag(self, tag, clean_attrs)
def unknown_endtag(self, tag):
@@ -2689,10 +2681,12 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
if tag in self.unacceptable_elements_with_end_tag:
self.unacceptablestack -= 1
if self.mathmlOK and tag in self.mathml_elements:
- if tag == 'math' and self.mathmlOK: self.mathmlOK -= 1
+ if tag == 'math' and self.mathmlOK:
+ self.mathmlOK -= 1
elif self.svgOK and tag in self.svg_elements:
tag = self.svg_elem_map.get(tag,tag)
- if tag == 'svg' and self.svgOK: self.svgOK -= 1
+ if tag == 'svg' and self.svgOK:
+ self.svgOK -= 1
else:
return
_BaseHTMLProcessor.unknown_endtag(self, tag)
@@ -2712,24 +2706,27 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style)
# gauntlet
- if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): return ''
+ if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style):
+ return ''
# This replaced a regexp that used re.match and was prone to pathological back-tracking.
- if re.sub("\s*[-\w]+\s*:\s*[^:;]*;?", '', style).strip(): return ''
+ if re.sub("\s*[-\w]+\s*:\s*[^:;]*;?", '', style).strip():
+ return ''
clean = []
for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style):
- if not value: continue
- if prop.lower() in self.acceptable_css_properties:
- clean.append(prop + ': ' + value + ';')
- elif prop.split('-')[0].lower() in ['background','border','margin','padding']:
- for keyword in value.split():
- if not keyword in self.acceptable_css_keywords and \
- not self.valid_css_values.match(keyword):
- break
- else:
- clean.append(prop + ': ' + value + ';')
- elif self.svgOK and prop.lower() in self.acceptable_svg_properties:
- clean.append(prop + ': ' + value + ';')
+ if not value:
+ continue
+ if prop.lower() in self.acceptable_css_properties:
+ clean.append(prop + ': ' + value + ';')
+ elif prop.split('-')[0].lower() in ['background','border','margin','padding']:
+ for keyword in value.split():
+ if not keyword in self.acceptable_css_keywords and \
+ not self.valid_css_values.match(keyword):
+ break
+ else:
+ clean.append(prop + ': ' + value + ';')
+ elif self.svgOK and prop.lower() in self.acceptable_svg_properties:
+ clean.append(prop + ': ' + value + ';')
return ' '.join(clean)
@@ -2747,98 +2744,57 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
def _sanitizeHTML(htmlSource, encoding, _type):
+ if not _SGML_AVAILABLE:
+ return htmlSource
p = _HTMLSanitizer(encoding, _type)
htmlSource = htmlSource.replace(''):
- data = data.split('>', 1)[1]
- if data.count('