and
+ # only if all the remaining content is nested underneath it.
+ # This means that the divs would be retained in the following:
+ #
+ while pieces and len(pieces) > 1 and not pieces[-1].strip():
+ del pieces[-1]
+ while pieces and len(pieces) > 1 and not pieces[0].strip():
+ del pieces[0]
+ if pieces and (pieces[0] == '
' or pieces[0].startswith('
':
+ depth = 0
+ for piece in pieces[:-1]:
+ if piece.startswith(''):
+ depth -= 1
+ if depth == 0:
+ break
+ elif piece.startswith('<') and not piece.endswith('/>'):
+ depth += 1
+ else:
+ pieces = pieces[1:-1]
+
+ output = ''.join(pieces)
+ if strip_whitespace:
+ output = output.strip()
+ if not expecting_text:
+ return output
+
+ # decode base64 content
+ if base64 and self.contentparams.get('base64', 0):
+ try:
+ output = _base64decode(output)
+ except binascii.Error:
+ pass
+ except binascii.Incomplete:
+ pass
+ except TypeError:
+ # In Python 3, base64 takes and outputs bytes, not str
+ # This may not be the most correct way to accomplish this
+ output = _base64decode(output.encode('utf-8')).decode('utf-8')
+
+ # resolve relative URIs
+ if (element in self.can_be_relative_uri) and output:
+ # do not resolve guid elements with isPermalink="false"
+ if not element == 'id' or self.guidislink:
+ output = self.resolve_uri(output)
+
+ # decode entities within embedded markup
+ if not self.contentparams.get('base64', 0):
+ output = self.decode_entities(element, output)
+
+ # 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.looks_like_html(output):
+ self.contentparams['type'] = 'text/html'
+
+ # remove temporary cruft from contentparams
+ try:
+ del self.contentparams['mode']
+ except KeyError:
+ pass
+ try:
+ del self.contentparams['base64']
+ except KeyError:
+ pass
+
+ is_htmlish = self.map_content_type(self.contentparams.get('type', 'text/html')) in self.html_types
+ # resolve relative URIs within embedded markup
+ if is_htmlish and self.resolve_relative_uris:
+ if element in self.can_contain_relative_uris:
+ output = resolve_relative_uris(output, self.baseuri, self.encoding, self.contentparams.get('type', 'text/html'))
+
+ # sanitize embedded markup
+ if is_htmlish and self.sanitize_html:
+ if element in self.can_contain_dangerous_markup:
+ output = _sanitize_html(output, self.encoding, self.contentparams.get('type', 'text/html'))
+
+ if self.encoding and isinstance(output, bytes_):
+ 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 not isinstance(output, bytes_):
+ try:
+ output = output.encode('iso-8859-1').decode('utf-8')
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ pass
+
+ # map win-1252 extensions to the proper code points
+ if not isinstance(output, bytes_):
+ output = output.translate(_cp1252)
+
+ # 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 -1 < self.title_depth <= self.depth:
+ return output
+
+ # store output in appropriate place(s)
+ if self.inentry and not self.insource:
+ if element == 'content':
+ self.entries[-1].setdefault(element, [])
+ contentparams = copy.deepcopy(self.contentparams)
+ contentparams['value'] = output
+ self.entries[-1][element].append(contentparams)
+ elif element == 'link':
+ if not self.inimage:
+ # 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_]+);", r"&\g<1>", output)
+ self.entries[-1][element] = output
+ if output:
+ self.entries[-1]['links'][-1]['href'] = output
+ else:
+ if element == 'description':
+ element = 'summary'
+ 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
+ self.entries[-1][element + '_detail'] = contentparams
+ elif self.infeed or self.insource: # and (not self.intextinput) and (not self.inimage):
+ context = self._get_context()
+ if element == 'description':
+ element = 'subtitle'
+ context[element] = output
+ if element == 'link':
+ # fix query variables; see above for the explanation
+ output = re.sub("&([A-Za-z0-9_]+);", r"&\g<1>", output)
+ context[element] = output
+ context['links'][-1]['href'] = output
+ elif self.incontent:
+ contentparams = copy.deepcopy(self.contentparams)
+ contentparams['value'] = output
+ context[element + '_detail'] = contentparams
+ return output
+
+ def push_content(self, tag, attrs_d, default_content_type, expecting_text):
+ self.incontent += 1
+ if self.lang:
+ self.lang = self.lang.replace('_', '-')
+ self.contentparams = FeedParserDict({
+ 'type': self.map_content_type(attrs_d.get('type', default_content_type)),
+ 'language': self.lang,
+ 'base': self.baseuri})
+ self.contentparams['base64'] = self._is_base64(attrs_d, self.contentparams)
+ self.push(tag, expecting_text)
+
+ def pop_content(self, tag):
+ value = self.pop(tag)
+ self.incontent -= 1
+ self.contentparams.clear()
+ return value
+
+ # a number of elements in a number of RSS variants are nominally plain
+ # 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.
+ @staticmethod
+ def looks_like_html(s):
+ """
+ :type s: str
+ :rtype: bool
+ """
+
+ # must have a close tag or an entity reference to qualify
+ if not (re.search(r'(\w+)>', s) or re.search(r'?\w+;', s)):
+ return False
+
+ # all tags must be in a restricted subset of valid HTML tags
+ if any((t for t in re.findall(r'?(\w+)', s) if t.lower() not in _HTMLSanitizer.acceptable_elements)):
+ return False
+
+ # all entities must have been defined as valid HTML entities
+ if any((e for e in re.findall(r'&(\w+);', s) if e not in entitydefs)):
+ return False
+
+ return True
+
+ def _map_to_standard_prefix(self, name):
+ colonpos = name.find(':')
+ if colonpos != -1:
+ prefix = name[:colonpos]
+ suffix = name[colonpos+1:]
+ prefix = self.namespacemap.get(prefix, prefix)
+ name = prefix + ':' + suffix
+ return name
+
+ def _get_attribute(self, attrs_d, name):
+ return attrs_d.get(self._map_to_standard_prefix(name))
+
+ def _is_base64(self, attrs_d, contentparams):
+ if attrs_d.get('mode', '') == 'base64':
+ return 1
+ if self.contentparams['type'].startswith('text/'):
+ return 0
+ if self.contentparams['type'].endswith('+xml'):
+ return 0
+ if self.contentparams['type'].endswith('/xml'):
+ return 0
+ return 1
+
+ @staticmethod
+ def _enforce_href(attrs_d):
+ href = attrs_d.get('url', attrs_d.get('uri', attrs_d.get('href', None)))
+ if href:
+ try:
+ del attrs_d['url']
+ except KeyError:
+ pass
+ try:
+ del attrs_d['uri']
+ except KeyError:
+ pass
+ attrs_d['href'] = href
+ return attrs_d
+
+ def _save(self, key, value, overwrite=False):
+ context = self._get_context()
+ if overwrite:
+ context[key] = value
+ else:
+ context.setdefault(key, value)
+
+ def _get_context(self):
+ if self.insource:
+ context = self.sourcedata
+ elif self.inimage and 'image' in self.feeddata:
+ context = self.feeddata['image']
+ elif self.intextinput:
+ context = self.feeddata['textinput']
+ elif self.inentry:
+ context = self.entries[-1]
+ else:
+ context = self.feeddata
+ return context
+
+ def _save_author(self, key, value, prefix='author'):
+ context = self._get_context()
+ context.setdefault(prefix + '_detail', FeedParserDict())
+ context[prefix + '_detail'][key] = value
+ self._sync_author_detail()
+ context.setdefault('authors', [FeedParserDict()])
+ context['authors'][-1][key] = value
+
+ def _save_contributor(self, key, value):
+ context = self._get_context()
+ context.setdefault('contributors', [FeedParserDict()])
+ context['contributors'][-1][key] = value
+
+ def _sync_author_detail(self, key='author'):
+ context = self._get_context()
+ detail = context.get('%ss' % key, [FeedParserDict()])[-1]
+ if detail:
+ name = detail.get('name')
+ email = detail.get('email')
+ if name and email:
+ context[key] = '%s (%s)' % (name, email)
+ elif name:
+ context[key] = name
+ elif email:
+ context[key] = email
+ else:
+ author, email = context.get(key), None
+ 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)
+ # probably a better way to do the following, but it passes
+ # all the tests
+ author = author.replace(email, '')
+ author = author.replace('()', '')
+ author = author.replace('<>', '')
+ author = author.replace('<>', '')
+ author = author.strip()
+ if author and (author[0] == '('):
+ author = author[1:]
+ if author and (author[-1] == ')'):
+ author = author[:-1]
+ author = author.strip()
+ if author or email:
+ context.setdefault('%s_detail' % key, detail)
+ if author:
+ detail['name'] = author
+ if email:
+ detail['email'] = email
+
+ def _add_tag(self, term, scheme, label):
+ context = self._get_context()
+ tags = context.setdefault('tags', [])
+ 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, attrs_d):
+ # 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._add_tag(term.strip(), None, None)
diff --git a/vendor/feedparser/namespaces/__init__.py b/vendor/feedparser/namespaces/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/vendor/feedparser/namespaces/_base.py b/vendor/feedparser/namespaces/_base.py
new file mode 100644
index 000000000..a4b99c694
--- /dev/null
+++ b/vendor/feedparser/namespaces/_base.py
@@ -0,0 +1,506 @@
+# Support for the Atom, RSS, RDF, and CDF feed formats
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import copy
+
+from ..datetimes import _parse_date
+from ..urls import make_safe_absolute_uri
+from ..util import FeedParserDict
+
+
+class Namespace(object):
+ """Support for the Atom, RSS, RDF, and CDF feed formats.
+
+ The feed formats all share common elements, some of which have conflicting
+ interpretations. For simplicity, all of the base feed format support is
+ collected here.
+ """
+
+ supported_namespaces = {
+ '': '',
+ 'http://backend.userland.com/rss': '',
+ 'http://blogs.law.harvard.edu/tech/rss': '',
+ 'http://purl.org/rss/1.0/': '',
+ 'http://my.netscape.com/rdf/simple/0.9/': '',
+ 'http://example.com/newformat#': '',
+ 'http://example.com/necho': '',
+ 'http://purl.org/echo/': '',
+ 'uri/of/echo/namespace#': '',
+ 'http://purl.org/pie/': '',
+ 'http://purl.org/atom/ns#': '',
+ 'http://www.w3.org/2005/Atom': '',
+ 'http://purl.org/rss/1.0/modules/rss091#': '',
+ }
+
+ def _start_rss(self, attrs_d):
+ versionmap = {
+ '0.91': 'rss091u',
+ '0.92': 'rss092',
+ '0.93': 'rss093',
+ '0.94': 'rss094',
+ }
+
+ # If we're here then this is an RSS feed.
+ # If we don't have a version or have a version that starts with something
+ # other than RSS then there's been a mistake. Correct it.
+ if not self.version or not self.version.startswith('rss'):
+ attr_version = attrs_d.get('version', '')
+ version = versionmap.get(attr_version)
+ if version:
+ self.version = version
+ elif attr_version.startswith('2.'):
+ self.version = 'rss20'
+ else:
+ self.version = 'rss'
+
+ def _start_channel(self, attrs_d):
+ self.infeed = 1
+ self._cdf_common(attrs_d)
+
+ def _cdf_common(self, attrs_d):
+ if 'lastmod' in attrs_d:
+ self._start_modified({})
+ self.elementstack[-1][-1] = attrs_d['lastmod']
+ self._end_modified()
+ if 'href' in attrs_d:
+ self._start_link({})
+ self.elementstack[-1][-1] = attrs_d['href']
+ self._end_link()
+
+ def _start_feed(self, attrs_d):
+ self.infeed = 1
+ versionmap = {'0.1': 'atom01',
+ '0.2': 'atom02',
+ '0.3': 'atom03'}
+ if not self.version:
+ attr_version = attrs_d.get('version')
+ version = versionmap.get(attr_version)
+ if version:
+ self.version = version
+ else:
+ self.version = 'atom'
+
+ def _end_channel(self):
+ self.infeed = 0
+ _end_feed = _end_channel
+
+ def _start_image(self, attrs_d):
+ context = self._get_context()
+ if not self.inentry:
+ context.setdefault('image', FeedParserDict())
+ self.inimage = 1
+ self.title_depth = -1
+ self.push('image', 0)
+
+ def _end_image(self):
+ self.pop('image')
+ self.inimage = 0
+
+ def _start_textinput(self, attrs_d):
+ context = self._get_context()
+ context.setdefault('textinput', FeedParserDict())
+ self.intextinput = 1
+ self.title_depth = -1
+ self.push('textinput', 0)
+ _start_textInput = _start_textinput
+
+ def _end_textinput(self):
+ self.pop('textinput')
+ self.intextinput = 0
+ _end_textInput = _end_textinput
+
+ def _start_author(self, attrs_d):
+ self.inauthor = 1
+ self.push('author', 1)
+ # Append a new FeedParserDict when expecting an author
+ context = self._get_context()
+ context.setdefault('authors', [])
+ context['authors'].append(FeedParserDict())
+ _start_managingeditor = _start_author
+
+ def _end_author(self):
+ self.pop('author')
+ self.inauthor = 0
+ self._sync_author_detail()
+ _end_managingeditor = _end_author
+
+ def _start_contributor(self, attrs_d):
+ self.incontributor = 1
+ context = self._get_context()
+ context.setdefault('contributors', [])
+ context['contributors'].append(FeedParserDict())
+ self.push('contributor', 0)
+
+ def _end_contributor(self):
+ self.pop('contributor')
+ self.incontributor = 0
+
+ def _start_name(self, attrs_d):
+ self.push('name', 0)
+
+ def _end_name(self):
+ value = self.pop('name')
+ if self.inpublisher:
+ self._save_author('name', value, 'publisher')
+ elif self.inauthor:
+ self._save_author('name', value)
+ elif self.incontributor:
+ self._save_contributor('name', value)
+ elif self.intextinput:
+ context = self._get_context()
+ context['name'] = value
+
+ def _start_width(self, attrs_d):
+ self.push('width', 0)
+
+ def _end_width(self):
+ value = self.pop('width')
+ try:
+ value = int(value)
+ except ValueError:
+ value = 0
+ if self.inimage:
+ context = self._get_context()
+ context['width'] = value
+
+ def _start_height(self, attrs_d):
+ self.push('height', 0)
+
+ def _end_height(self):
+ value = self.pop('height')
+ try:
+ value = int(value)
+ except ValueError:
+ value = 0
+ if self.inimage:
+ context = self._get_context()
+ context['height'] = value
+
+ def _start_url(self, attrs_d):
+ self.push('href', 1)
+ _start_homepage = _start_url
+ _start_uri = _start_url
+
+ def _end_url(self):
+ value = self.pop('href')
+ if self.inauthor:
+ self._save_author('href', value)
+ elif self.incontributor:
+ self._save_contributor('href', value)
+ _end_homepage = _end_url
+ _end_uri = _end_url
+
+ def _start_email(self, attrs_d):
+ self.push('email', 0)
+
+ def _end_email(self):
+ value = self.pop('email')
+ if self.inpublisher:
+ self._save_author('email', value, 'publisher')
+ elif self.inauthor:
+ self._save_author('email', value)
+ elif self.incontributor:
+ self._save_contributor('email', value)
+
+ def _start_subtitle(self, attrs_d):
+ self.push_content('subtitle', attrs_d, 'text/plain', 1)
+ _start_tagline = _start_subtitle
+
+ def _end_subtitle(self):
+ self.pop_content('subtitle')
+ _end_tagline = _end_subtitle
+
+ def _start_rights(self, attrs_d):
+ self.push_content('rights', attrs_d, 'text/plain', 1)
+ _start_copyright = _start_rights
+
+ def _end_rights(self):
+ self.pop_content('rights')
+ _end_copyright = _end_rights
+
+ def _start_item(self, attrs_d):
+ self.entries.append(FeedParserDict())
+ self.push('item', 0)
+ self.inentry = 1
+ self.guidislink = 0
+ self.title_depth = -1
+ id = self._get_attribute(attrs_d, 'rdf:about')
+ if id:
+ context = self._get_context()
+ context['id'] = id
+ self._cdf_common(attrs_d)
+ _start_entry = _start_item
+
+ def _end_item(self):
+ self.pop('item')
+ self.inentry = 0
+ _end_entry = _end_item
+
+ def _start_language(self, attrs_d):
+ self.push('language', 1)
+
+ def _end_language(self):
+ self.lang = self.pop('language')
+
+ def _start_webmaster(self, attrs_d):
+ self.push('publisher', 1)
+
+ def _end_webmaster(self):
+ self.pop('publisher')
+ self._sync_author_detail('publisher')
+
+ def _start_published(self, attrs_d):
+ self.push('published', 1)
+ _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_issued = _end_published
+ _end_pubdate = _end_published
+
+ def _start_updated(self, attrs_d):
+ self.push('updated', 1)
+ _start_modified = _start_updated
+ _start_lastbuilddate = _start_updated
+
+ def _end_updated(self):
+ value = self.pop('updated')
+ parsed_value = _parse_date(value)
+ self._save('updated_parsed', parsed_value, overwrite=True)
+ _end_modified = _end_updated
+ _end_lastbuilddate = _end_updated
+
+ def _start_created(self, attrs_d):
+ self.push('created', 1)
+
+ def _end_created(self):
+ value = self.pop('created')
+ self._save('created_parsed', _parse_date(value), overwrite=True)
+
+ def _start_expirationdate(self, attrs_d):
+ self.push('expired', 1)
+
+ def _end_expirationdate(self):
+ self._save('expired_parsed', _parse_date(self.pop('expired')), overwrite=True)
+
+ def _start_category(self, attrs_d):
+ term = attrs_d.get('term')
+ scheme = attrs_d.get('scheme', attrs_d.get('domain'))
+ label = attrs_d.get('label')
+ self._add_tag(term, scheme, label)
+ self.push('category', 1)
+ _start_keywords = _start_category
+
+ def _end_category(self):
+ value = self.pop('category')
+ if not value:
+ return
+ context = self._get_context()
+ tags = context['tags']
+ if value and len(tags) and not tags[-1]['term']:
+ tags[-1]['term'] = value
+ else:
+ self._add_tag(value, None, None)
+ _end_keywords = _end_category
+
+ def _start_cloud(self, attrs_d):
+ self._get_context()['cloud'] = FeedParserDict(attrs_d)
+
+ def _start_link(self, attrs_d):
+ attrs_d.setdefault('rel', 'alternate')
+ if attrs_d['rel'] == 'self':
+ attrs_d.setdefault('type', 'application/atom+xml')
+ else:
+ attrs_d.setdefault('type', 'text/html')
+ context = self._get_context()
+ attrs_d = self._enforce_href(attrs_d)
+ if 'href' in attrs_d:
+ attrs_d['href'] = self.resolve_uri(attrs_d['href'])
+ expecting_text = self.infeed or self.inentry or self.insource
+ context.setdefault('links', [])
+ if not (self.inentry and self.inimage):
+ context['links'].append(FeedParserDict(attrs_d))
+ if 'href' in attrs_d:
+ if (
+ attrs_d.get('rel') == 'alternate'
+ and self.map_content_type(attrs_d.get('type')) in self.html_types
+ ):
+ context['link'] = attrs_d['href']
+ else:
+ self.push('link', expecting_text)
+
+ def _end_link(self):
+ self.pop('link')
+
+ def _start_guid(self, attrs_d):
+ self.guidislink = (attrs_d.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 'link' not in self._get_context())
+ 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, attrs_d):
+ if self.svgOK:
+ return self.unknown_starttag('title', list(attrs_d.items()))
+ self.push_content('title', attrs_d, 'text/plain', self.infeed or self.inentry or self.insource)
+
+ def _end_title(self):
+ if self.svgOK:
+ return
+ value = self.pop_content('title')
+ if not value:
+ return
+ self.title_depth = self.depth
+
+ def _start_description(self, attrs_d):
+ context = self._get_context()
+ if 'summary' in context:
+ self._summaryKey = 'content'
+ self._start_content(attrs_d)
+ else:
+ self.push_content('description', attrs_d, 'text/html', self.infeed or self.inentry or self.insource)
+
+ def _start_abstract(self, attrs_d):
+ self.push_content('description', attrs_d, 'text/plain', self.infeed or self.inentry or self.insource)
+
+ def _end_description(self):
+ if self._summaryKey == 'content':
+ self._end_content()
+ else:
+ self.pop_content('description')
+ self._summaryKey = None
+ _end_abstract = _end_description
+
+ def _start_info(self, attrs_d):
+ self.push_content('info', attrs_d, 'text/plain', 1)
+ _start_feedburner_browserfriendly = _start_info
+
+ def _end_info(self):
+ self.pop_content('info')
+ _end_feedburner_browserfriendly = _end_info
+
+ def _start_generator(self, attrs_d):
+ if attrs_d:
+ attrs_d = self._enforce_href(attrs_d)
+ if 'href' in attrs_d:
+ attrs_d['href'] = self.resolve_uri(attrs_d['href'])
+ self._get_context()['generator_detail'] = FeedParserDict(attrs_d)
+ self.push('generator', 1)
+
+ def _end_generator(self):
+ value = self.pop('generator')
+ context = self._get_context()
+ if 'generator_detail' in context:
+ context['generator_detail']['name'] = value
+
+ def _start_summary(self, attrs_d):
+ context = self._get_context()
+ if 'summary' in context:
+ self._summaryKey = 'content'
+ self._start_content(attrs_d)
+ else:
+ self._summaryKey = 'summary'
+ self.push_content(self._summaryKey, attrs_d, 'text/plain', 1)
+
+ def _end_summary(self):
+ if self._summaryKey == 'content':
+ self._end_content()
+ else:
+ self.pop_content(self._summaryKey or 'summary')
+ self._summaryKey = None
+
+ def _start_enclosure(self, attrs_d):
+ attrs_d = self._enforce_href(attrs_d)
+ context = self._get_context()
+ attrs_d['rel'] = 'enclosure'
+ context.setdefault('links', []).append(FeedParserDict(attrs_d))
+
+ def _start_source(self, attrs_d):
+ if 'url' in attrs_d:
+ # This means that we're processing a source element from an RSS 2.0 feed
+ self.sourcedata['href'] = attrs_d['url']
+ self.push('source', 1)
+ self.insource = 1
+ self.title_depth = -1
+
+ def _end_source(self):
+ self.insource = 0
+ value = self.pop('source')
+ if value:
+ self.sourcedata['title'] = value
+ self._get_context()['source'] = copy.deepcopy(self.sourcedata)
+ self.sourcedata.clear()
+
+ def _start_content(self, attrs_d):
+ self.push_content('content', attrs_d, 'text/plain', 1)
+ src = attrs_d.get('src')
+ if src:
+ self.contentparams['src'] = src
+ self.push('content', 1)
+
+ def _start_body(self, attrs_d):
+ self.push_content('content', attrs_d, 'application/xhtml+xml', 1)
+ _start_xhtml_body = _start_body
+
+ def _start_content_encoded(self, attrs_d):
+ self.push_content('content', attrs_d, 'text/html', 1)
+ _start_fullitem = _start_content_encoded
+
+ def _end_content(self):
+ copyToSummary = self.map_content_type(self.contentparams.get('type')) in ({'text/plain'} | self.html_types)
+ value = self.pop_content('content')
+ if copyToSummary:
+ self._save('summary', value)
+
+ _end_body = _end_content
+ _end_xhtml_body = _end_content
+ _end_content_encoded = _end_content
+ _end_fullitem = _end_content
+
+ def _start_newlocation(self, attrs_d):
+ self.push('newlocation', 1)
+
+ def _end_newlocation(self):
+ url = self.pop('newlocation')
+ context = self._get_context()
+ # don't set newlocation if the context isn't right
+ if context is not self.feeddata:
+ return
+ context['newlocation'] = make_safe_absolute_uri(self.baseuri, url.strip())
diff --git a/vendor/feedparser/namespaces/admin.py b/vendor/feedparser/namespaces/admin.py
new file mode 100644
index 000000000..8d7b67c5d
--- /dev/null
+++ b/vendor/feedparser/namespaces/admin.py
@@ -0,0 +1,56 @@
+# Support for the administrative elements extension
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+from ..util import FeedParserDict
+
+
+class Namespace(object):
+ # RDF Site Summary 1.0 Modules: Administrative
+ # http://web.resource.org/rss/1.0/modules/admin/
+
+ supported_namespaces = {
+ 'http://webns.net/mvcb/': 'admin',
+ }
+
+ def _start_admin_generatoragent(self, attrs_d):
+ self.push('generator', 1)
+ value = self._get_attribute(attrs_d, 'rdf:resource')
+ if value:
+ self.elementstack[-1][2].append(value)
+ self.pop('generator')
+ self._get_context()['generator_detail'] = FeedParserDict({'href': value})
+
+ def _start_admin_errorreportsto(self, attrs_d):
+ self.push('errorreportsto', 1)
+ value = self._get_attribute(attrs_d, 'rdf:resource')
+ if value:
+ self.elementstack[-1][2].append(value)
+ self.pop('errorreportsto')
diff --git a/vendor/feedparser/namespaces/cc.py b/vendor/feedparser/namespaces/cc.py
new file mode 100644
index 000000000..3fc510609
--- /dev/null
+++ b/vendor/feedparser/namespaces/cc.py
@@ -0,0 +1,72 @@
+# Support for the Creative Commons licensing extensions
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+from ..util import FeedParserDict
+
+
+class Namespace(object):
+ supported_namespaces = {
+ # RDF-based namespace
+ 'http://creativecommons.org/ns#license': 'cc',
+
+ # Old RDF-based namespace
+ 'http://web.resource.org/cc/': 'cc',
+
+ # RSS-based namespace
+ 'http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html': 'creativecommons',
+
+ # Old RSS-based namespace
+ 'http://backend.userland.com/creativeCommonsRssModule': 'creativecommons',
+ }
+
+ def _start_cc_license(self, attrs_d):
+ context = self._get_context()
+ value = self._get_attribute(attrs_d, 'rdf:resource')
+ attrs_d = FeedParserDict()
+ attrs_d['rel'] = 'license'
+ if value:
+ attrs_d['href'] = value
+ context.setdefault('links', []).append(attrs_d)
+
+ def _start_creativecommons_license(self, attrs_d):
+ self.push('license', 1)
+ _start_creativeCommons_license = _start_creativecommons_license
+
+ def _end_creativecommons_license(self):
+ value = self.pop('license')
+ context = self._get_context()
+ attrs_d = FeedParserDict()
+ attrs_d['rel'] = 'license'
+ if value:
+ attrs_d['href'] = value
+ context.setdefault('links', []).append(attrs_d)
+ del context['license']
+ _end_creativeCommons_license = _end_creativecommons_license
diff --git a/vendor/feedparser/namespaces/dc.py b/vendor/feedparser/namespaces/dc.py
new file mode 100644
index 000000000..938477b35
--- /dev/null
+++ b/vendor/feedparser/namespaces/dc.py
@@ -0,0 +1,137 @@
+# Support for the Dublin Core metadata extensions
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+from ..datetimes import _parse_date
+from ..util import FeedParserDict
+
+
+class Namespace(object):
+ supported_namespaces = {
+ 'http://purl.org/dc/elements/1.1/': 'dc',
+ 'http://purl.org/dc/terms/': 'dcterms',
+ }
+
+ def _end_dc_author(self):
+ self._end_author()
+
+ def _end_dc_creator(self):
+ self._end_author()
+
+ def _end_dc_date(self):
+ self._end_updated()
+
+ def _end_dc_description(self):
+ self._end_description()
+
+ def _end_dc_language(self):
+ self._end_language()
+
+ def _end_dc_publisher(self):
+ self._end_webmaster()
+
+ def _end_dc_rights(self):
+ self._end_rights()
+
+ def _end_dc_subject(self):
+ self._end_category()
+
+ def _end_dc_title(self):
+ self._end_title()
+
+ def _end_dcterms_created(self):
+ self._end_created()
+
+ def _end_dcterms_issued(self):
+ self._end_published()
+
+ def _end_dcterms_modified(self):
+ self._end_updated()
+
+ def _start_dc_author(self, attrs_d):
+ self._start_author(attrs_d)
+
+ def _start_dc_creator(self, attrs_d):
+ self._start_author(attrs_d)
+
+ def _start_dc_date(self, attrs_d):
+ self._start_updated(attrs_d)
+
+ def _start_dc_description(self, attrs_d):
+ self._start_description(attrs_d)
+
+ def _start_dc_language(self, attrs_d):
+ self._start_language(attrs_d)
+
+ def _start_dc_publisher(self, attrs_d):
+ self._start_webmaster(attrs_d)
+
+ def _start_dc_rights(self, attrs_d):
+ self._start_rights(attrs_d)
+
+ def _start_dc_subject(self, attrs_d):
+ self._start_category(attrs_d)
+
+ def _start_dc_title(self, attrs_d):
+ self._start_title(attrs_d)
+
+ def _start_dcterms_created(self, attrs_d):
+ self._start_created(attrs_d)
+
+ def _start_dcterms_issued(self, attrs_d):
+ self._start_published(attrs_d)
+
+ def _start_dcterms_modified(self, attrs_d):
+ self._start_updated(attrs_d)
+
+ def _start_dcterms_valid(self, attrs_d):
+ 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_dc_contributor(self, attrs_d):
+ self.incontributor = 1
+ context = self._get_context()
+ context.setdefault('contributors', [])
+ context['contributors'].append(FeedParserDict())
+ self.push('name', 0)
+
+ def _end_dc_contributor(self):
+ self._end_name()
+ self.incontributor = 0
diff --git a/vendor/feedparser/namespaces/georss.py b/vendor/feedparser/namespaces/georss.py
new file mode 100644
index 000000000..f89094b84
--- /dev/null
+++ b/vendor/feedparser/namespaces/georss.py
@@ -0,0 +1,277 @@
+# Support for the GeoRSS format
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+from __future__ import generator_stop
+
+from ..util import FeedParserDict
+
+
+class Namespace(object):
+ supported_namespaces = {
+ 'http://www.w3.org/2003/01/geo/wgs84_pos#': 'geo',
+ 'http://www.georss.org/georss': 'georss',
+ 'http://www.opengis.net/gml': 'gml',
+ }
+
+ def __init__(self):
+ self.ingeometry = 0
+ super(Namespace, self).__init__()
+
+ def _start_georssgeom(self, attrs_d):
+ self.push('geometry', 0)
+ context = self._get_context()
+ 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._get_context()
+ 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, attrs_d):
+ self.push('where', 0)
+ context = self._get_context()
+ context['where'] = FeedParserDict()
+ _start_georss_where = _start_where
+
+ def _parse_srs_attrs(self, attrs_d):
+ srs_name = attrs_d.get('srsname')
+ try:
+ srs_dimension = int(attrs_d.get('srsdimension', '2'))
+ except ValueError:
+ srs_dimension = 2
+ context = self._get_context()
+ context['where']['srsName'] = srs_name
+ context['where']['srsDimension'] = srs_dimension
+
+ def _start_gml_point(self, attrs_d):
+ self._parse_srs_attrs(attrs_d)
+ self.ingeometry = 1
+ self.push('geometry', 0)
+
+ def _start_gml_linestring(self, attrs_d):
+ self._parse_srs_attrs(attrs_d)
+ self.ingeometry = 'linestring'
+ self.push('geometry', 0)
+
+ def _start_gml_polygon(self, attrs_d):
+ self._parse_srs_attrs(attrs_d)
+ self.push('geometry', 0)
+
+ def _start_gml_exterior(self, attrs_d):
+ self.push('geometry', 0)
+
+ def _start_gml_linearring(self, attrs_d):
+ self.ingeometry = 'polygon'
+ self.push('geometry', 0)
+
+ def _start_gml_pos(self, attrs_d):
+ self.push('pos', 0)
+
+ def _end_gml_pos(self):
+ this = self.pop('pos')
+ context = self._get_context()
+ srs_name = context['where'].get('srsName')
+ srs_dimension = context['where'].get('srsDimension', 2)
+ swap = True
+ if srs_name and "EPSG" in srs_name:
+ epsg = int(srs_name.split(":")[-1])
+ swap = bool(epsg in _geogCS)
+ geometry = _parse_georss_point(this, swap=swap, dims=srs_dimension)
+ if geometry:
+ self._save_where(geometry)
+
+ def _start_gml_poslist(self, attrs_d):
+ self.push('pos', 0)
+
+ def _end_gml_poslist(self):
+ this = self.pop('pos')
+ context = self._get_context()
+ srs_name = context['where'].get('srsName')
+ srs_dimension = context['where'].get('srsDimension', 2)
+ swap = True
+ if srs_name and "EPSG" in srs_name:
+ epsg = int(srs_name.split(":")[-1])
+ swap = bool(epsg in _geogCS)
+ geometry = _parse_poslist(
+ this, self.ingeometry, swap=swap, dims=srs_dimension)
+ 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
+
+
+# GeoRSS geometry parsers. Each return a dict with 'type' and 'coordinates'
+# items, or None in the case of a parsing error.
+
+def _parse_poslist(value, geom_type, swap=True, dims=2):
+ if geom_type == 'linestring':
+ return _parse_georss_line(value, swap, dims)
+ elif geom_type == 'polygon':
+ ring = _parse_georss_line(value, swap, dims)
+ return {'type': 'Polygon', 'coordinates': (ring['coordinates'],)}
+ else:
+ return None
+
+
+def _gen_georss_coords(value, swap=True, dims=2):
+ # A generator of (lon, lat) pairs from a string of encoded GeoRSS
+ # coordinates. Converts to floats and swaps order.
+ latlons = (float(ll) for ll in value.replace(',', ' ').split())
+ while True:
+ try:
+ t = [next(latlons), next(latlons)][::swap and -1 or 1]
+ if dims == 3:
+ t.append(next(latlons))
+ yield tuple(t)
+ except StopIteration:
+ return
+
+
+def _parse_georss_point(value, swap=True, dims=2):
+ # A point contains a single latitude-longitude pair, separated by
+ # whitespace. We'll also handle comma separators.
+ try:
+ coords = list(_gen_georss_coords(value, swap, dims))
+ return {'type': 'Point', 'coordinates': coords[0]}
+ except (IndexError, ValueError):
+ return None
+
+
+def _parse_georss_line(value, swap=True, dims=2):
+ # A line contains a space separated list of latitude-longitude pairs in
+ # WGS84 coordinate reference system, with each pair separated by
+ # whitespace. There must be at least two pairs.
+ try:
+ coords = list(_gen_georss_coords(value, swap, dims))
+ return {'type': 'LineString', 'coordinates': coords}
+ except (IndexError, ValueError):
+ return None
+
+
+def _parse_georss_polygon(value, swap=True, dims=2):
+ # A polygon contains a space separated list of latitude-longitude pairs,
+ # with each pair separated by whitespace. There must be at least four
+ # pairs, with the last being identical to the first (so a polygon has a
+ # minimum of three actual points).
+ try:
+ ring = list(_gen_georss_coords(value, swap, dims))
+ except (IndexError, ValueError):
+ return None
+ if len(ring) < 4:
+ return None
+ return {'type': 'Polygon', 'coordinates': (ring,)}
+
+
+def _parse_georss_box(value, swap=True, dims=2):
+ # A bounding box is a rectangular region, often used to define the extents
+ # of a map or a rough area of interest. A box contains two space separate
+ # latitude-longitude pairs, with each pair separated by whitespace. The
+ # first pair is the lower corner, the second is the upper corner.
+ try:
+ coords = list(_gen_georss_coords(value, swap, dims))
+ return {'type': 'Box', 'coordinates': tuple(coords)}
+ except (IndexError, ValueError):
+ return None
+
+
+# The list of EPSG codes for geographic (latitude/longitude) coordinate
+# systems to support decoding of GeoRSS GML profiles.
+_geogCS = [
+ 3819, 3821, 3824, 3889, 3906, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008,
+ 4009, 4010, 4011, 4012, 4013, 4014, 4015, 4016, 4018, 4019, 4020, 4021, 4022,
+ 4023, 4024, 4025, 4027, 4028, 4029, 4030, 4031, 4032, 4033, 4034, 4035, 4036,
+ 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4052, 4053, 4054, 4055, 4075, 4081,
+ 4120, 4121, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132,
+ 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4142, 4143, 4144, 4145,
+ 4146, 4147, 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158,
+ 4159, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171,
+ 4172, 4173, 4174, 4175, 4176, 4178, 4179, 4180, 4181, 4182, 4183, 4184, 4185,
+ 4188, 4189, 4190, 4191, 4192, 4193, 4194, 4195, 4196, 4197, 4198, 4199, 4200,
+ 4201, 4202, 4203, 4204, 4205, 4206, 4207, 4208, 4209, 4210, 4211, 4212, 4213,
+ 4214, 4215, 4216, 4218, 4219, 4220, 4221, 4222, 4223, 4224, 4225, 4226, 4227,
+ 4228, 4229, 4230, 4231, 4232, 4233, 4234, 4235, 4236, 4237, 4238, 4239, 4240,
+ 4241, 4242, 4243, 4244, 4245, 4246, 4247, 4248, 4249, 4250, 4251, 4252, 4253,
+ 4254, 4255, 4256, 4257, 4258, 4259, 4260, 4261, 4262, 4263, 4264, 4265, 4266,
+ 4267, 4268, 4269, 4270, 4271, 4272, 4273, 4274, 4275, 4276, 4277, 4278, 4279,
+ 4280, 4281, 4282, 4283, 4284, 4285, 4286, 4287, 4288, 4289, 4291, 4292, 4293,
+ 4294, 4295, 4296, 4297, 4298, 4299, 4300, 4301, 4302, 4303, 4304, 4306, 4307,
+ 4308, 4309, 4310, 4311, 4312, 4313, 4314, 4315, 4316, 4317, 4318, 4319, 4322,
+ 4324, 4326, 4463, 4470, 4475, 4483, 4490, 4555, 4558, 4600, 4601, 4602, 4603,
+ 4604, 4605, 4606, 4607, 4608, 4609, 4610, 4611, 4612, 4613, 4614, 4615, 4616,
+ 4617, 4618, 4619, 4620, 4621, 4622, 4623, 4624, 4625, 4626, 4627, 4628, 4629,
+ 4630, 4631, 4632, 4633, 4634, 4635, 4636, 4637, 4638, 4639, 4640, 4641, 4642,
+ 4643, 4644, 4645, 4646, 4657, 4658, 4659, 4660, 4661, 4662, 4663, 4664, 4665,
+ 4666, 4667, 4668, 4669, 4670, 4671, 4672, 4673, 4674, 4675, 4676, 4677, 4678,
+ 4679, 4680, 4681, 4682, 4683, 4684, 4685, 4686, 4687, 4688, 4689, 4690, 4691,
+ 4692, 4693, 4694, 4695, 4696, 4697, 4698, 4699, 4700, 4701, 4702, 4703, 4704,
+ 4705, 4706, 4707, 4708, 4709, 4710, 4711, 4712, 4713, 4714, 4715, 4716, 4717,
+ 4718, 4719, 4720, 4721, 4722, 4723, 4724, 4725, 4726, 4727, 4728, 4729, 4730,
+ 4731, 4732, 4733, 4734, 4735, 4736, 4737, 4738, 4739, 4740, 4741, 4742, 4743,
+ 4744, 4745, 4746, 4747, 4748, 4749, 4750, 4751, 4752, 4753, 4754, 4755, 4756,
+ 4757, 4758, 4759, 4760, 4761, 4762, 4763, 4764, 4765, 4801, 4802, 4803, 4804,
+ 4805, 4806, 4807, 4808, 4809, 4810, 4811, 4813, 4814, 4815, 4816, 4817, 4818,
+ 4819, 4820, 4821, 4823, 4824, 4901, 4902, 4903, 4904, 4979,
+]
diff --git a/vendor/feedparser/namespaces/itunes.py b/vendor/feedparser/namespaces/itunes.py
new file mode 100644
index 000000000..56ab0a630
--- /dev/null
+++ b/vendor/feedparser/namespaces/itunes.py
@@ -0,0 +1,112 @@
+# Support for the iTunes format
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+from ..util import FeedParserDict
+
+
+class Namespace(object):
+ supported_namespaces = {
+ # Canonical namespace
+ 'http://www.itunes.com/DTDs/PodCast-1.0.dtd': 'itunes',
+
+ # Extra namespace
+ 'http://example.com/DTDs/PodCast-1.0.dtd': 'itunes',
+ }
+
+ def _start_itunes_author(self, attrs_d):
+ self._start_author(attrs_d)
+
+ def _end_itunes_author(self):
+ self._end_author()
+
+ def _end_itunes_category(self):
+ self._end_category()
+
+ def _start_itunes_name(self, attrs_d):
+ self._start_name(attrs_d)
+
+ def _end_itunes_name(self):
+ self._end_name()
+
+ def _start_itunes_email(self, attrs_d):
+ self._start_email(attrs_d)
+
+ def _end_itunes_email(self):
+ self._end_email()
+
+ def _start_itunes_subtitle(self, attrs_d):
+ self._start_subtitle(attrs_d)
+
+ def _end_itunes_subtitle(self):
+ self._end_subtitle()
+
+ def _start_itunes_summary(self, attrs_d):
+ self._start_summary(attrs_d)
+
+ def _end_itunes_summary(self):
+ self._end_summary()
+
+ def _start_itunes_owner(self, attrs_d):
+ self.inpublisher = 1
+ self.push('publisher', 0)
+
+ def _end_itunes_owner(self):
+ self.pop('publisher')
+ self.inpublisher = 0
+ self._sync_author_detail('publisher')
+
+ def _end_itunes_keywords(self):
+ for term in self.pop('itunes_keywords').split(','):
+ if term.strip():
+ self._add_tag(term.strip(), 'http://www.itunes.com/', None)
+
+ def _start_itunes_category(self, attrs_d):
+ self._add_tag(attrs_d.get('text'), 'http://www.itunes.com/', None)
+ self.push('category', 1)
+
+ def _start_itunes_image(self, attrs_d):
+ self.push('itunes_image', 0)
+ if attrs_d.get('href'):
+ self._get_context()['image'] = FeedParserDict({'href': attrs_d.get('href')})
+ elif attrs_d.get('url'):
+ self._get_context()['image'] = FeedParserDict({'href': attrs_d.get('url')})
+ _start_itunes_link = _start_itunes_image
+
+ def _end_itunes_block(self):
+ value = self.pop('itunes_block', 0)
+ self._get_context()['itunes_block'] = (value == 'yes') and 1 or 0
+
+ def _end_itunes_explicit(self):
+ value = self.pop('itunes_explicit', 0)
+ # Convert 'yes' -> True, 'clean' to False, and any other value to None
+ # False and None both evaluate as False, so the difference can be ignored
+ # by applications that only need to know if the content is explicit.
+ self._get_context()['itunes_explicit'] = (None, False, True)[(value == 'yes' and 2) or value == 'clean' or 0]
diff --git a/vendor/feedparser/namespaces/mediarss.py b/vendor/feedparser/namespaces/mediarss.py
new file mode 100644
index 000000000..48f9c7b90
--- /dev/null
+++ b/vendor/feedparser/namespaces/mediarss.py
@@ -0,0 +1,144 @@
+# Support for the Media RSS format
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+from ..util import FeedParserDict
+
+
+class Namespace(object):
+ supported_namespaces = {
+ # Canonical namespace
+ 'http://search.yahoo.com/mrss/': 'media',
+
+ # Old namespace (no trailing slash)
+ 'http://search.yahoo.com/mrss': 'media',
+ }
+
+ def _start_media_category(self, attrs_d):
+ attrs_d.setdefault('scheme', 'http://search.yahoo.com/mrss/category_schema')
+ self._start_category(attrs_d)
+
+ def _end_media_category(self):
+ self._end_category()
+
+ def _end_media_keywords(self):
+ for term in self.pop('media_keywords').split(','):
+ if term.strip():
+ self._add_tag(term.strip(), None, None)
+
+ def _start_media_title(self, attrs_d):
+ self._start_title(attrs_d)
+
+ def _end_media_title(self):
+ title_depth = self.title_depth
+ self._end_title()
+ self.title_depth = title_depth
+
+ def _start_media_group(self, attrs_d):
+ # don't do anything, but don't break the enclosed tags either
+ pass
+
+ def _start_media_rating(self, attrs_d):
+ context = self._get_context()
+ context.setdefault('media_rating', attrs_d)
+ self.push('rating', 1)
+
+ def _end_media_rating(self):
+ rating = self.pop('rating')
+ if rating is not None and rating.strip():
+ context = self._get_context()
+ context['media_rating']['content'] = rating
+
+ def _start_media_credit(self, attrs_d):
+ context = self._get_context()
+ context.setdefault('media_credit', [])
+ context['media_credit'].append(attrs_d)
+ self.push('credit', 1)
+
+ def _end_media_credit(self):
+ credit = self.pop('credit')
+ if credit is not None and credit.strip():
+ context = self._get_context()
+ context['media_credit'][-1]['content'] = credit
+
+ def _start_media_description(self, attrs_d):
+ self._start_description(attrs_d)
+
+ def _end_media_description(self):
+ self._end_description()
+
+ def _start_media_restriction(self, attrs_d):
+ context = self._get_context()
+ context.setdefault('media_restriction', attrs_d)
+ self.push('restriction', 1)
+
+ def _end_media_restriction(self):
+ restriction = self.pop('restriction')
+ if restriction is not None and restriction.strip():
+ context = self._get_context()
+ context['media_restriction']['content'] = [cc.strip().lower() for cc in restriction.split(' ')]
+
+ def _start_media_license(self, attrs_d):
+ context = self._get_context()
+ context.setdefault('media_license', attrs_d)
+ self.push('license', 1)
+
+ def _end_media_license(self):
+ license_ = self.pop('license')
+ if license_ is not None and license_.strip():
+ context = self._get_context()
+ context['media_license']['content'] = license_
+
+ def _start_media_content(self, attrs_d):
+ context = self._get_context()
+ context.setdefault('media_content', [])
+ context['media_content'].append(attrs_d)
+
+ def _start_media_thumbnail(self, attrs_d):
+ context = self._get_context()
+ context.setdefault('media_thumbnail', [])
+ self.push('url', 1) # new
+ context['media_thumbnail'].append(attrs_d)
+
+ def _end_media_thumbnail(self):
+ url = self.pop('url')
+ context = self._get_context()
+ if url is not None and url.strip():
+ if 'url' not in context['media_thumbnail'][-1]:
+ context['media_thumbnail'][-1]['url'] = url
+
+ def _start_media_player(self, attrs_d):
+ self.push('media_player', 0)
+ self._get_context()['media_player'] = FeedParserDict(attrs_d)
+
+ def _end_media_player(self):
+ value = self.pop('media_player')
+ context = self._get_context()
+ context['media_player']['content'] = value
diff --git a/vendor/feedparser/namespaces/psc.py b/vendor/feedparser/namespaces/psc.py
new file mode 100644
index 000000000..e04b37236
--- /dev/null
+++ b/vendor/feedparser/namespaces/psc.py
@@ -0,0 +1,77 @@
+# Support for the Podlove Simple Chapters format
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import datetime
+import re
+
+from .. import util
+
+
+class Namespace(object):
+ supported_namespaces = {
+ 'http://podlove.org/simple-chapters': 'psc',
+ }
+
+ def __init__(self):
+ # chapters will only be captured while psc_chapters_flag is True.
+ self.psc_chapters_flag = False
+ super(Namespace, self).__init__()
+
+ def _start_psc_chapters(self, attrs_d):
+ context = self._get_context()
+ if 'psc_chapters' not in context:
+ self.psc_chapters_flag = True
+ attrs_d['chapters'] = []
+ context['psc_chapters'] = util.FeedParserDict(attrs_d)
+
+ def _end_psc_chapters(self):
+ self.psc_chapters_flag = False
+
+ def _start_psc_chapter(self, attrs_d):
+ if self.psc_chapters_flag:
+ start = self._get_attribute(attrs_d, 'start')
+ attrs_d['start_parsed'] = _parse_psc_chapter_start(start)
+
+ context = self._get_context()['psc_chapters']
+ context['chapters'].append(util.FeedParserDict(attrs_d))
+
+
+format_ = re.compile(r'^((\d{2}):)?(\d{2}):(\d{2})(\.(\d{3}))?$')
+
+
+def _parse_psc_chapter_start(start):
+ m = format_.match(start)
+ if m is None:
+ return None
+
+ _, h, m, s, _, ms = m.groups()
+ h, m, s, ms = (int(h or 0), int(m), int(s), int(ms or 0))
+ return datetime.timedelta(0, h*60*60 + m*60 + s, ms*1000)
diff --git a/vendor/feedparser/parsers/__init__.py b/vendor/feedparser/parsers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/vendor/feedparser/parsers/loose.py b/vendor/feedparser/parsers/loose.py
new file mode 100644
index 000000000..398775542
--- /dev/null
+++ b/vendor/feedparser/parsers/loose.py
@@ -0,0 +1,81 @@
+# The loose feed parser that interfaces with an SGML parsing library
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+
+class _LooseFeedParser(object):
+ contentparams = None
+
+ def __init__(self, baseuri=None, baselang=None, encoding=None, entities=None):
+ self.baseuri = baseuri or ''
+ self.lang = baselang or None
+ self.encoding = encoding or 'utf-8' # character encoding
+ self.entities = entities or {}
+ super(_LooseFeedParser, self).__init__()
+
+ @staticmethod
+ def _normalize_attributes(kv):
+ k = kv[0].lower()
+ v = k in ('rel', 'type') and kv[1].lower() or kv[1]
+ # the sgml parser doesn't handle entities in attributes, nor
+ # does it pass the attribute values through as unicode, while
+ # strict xml parsers do -- account for this difference
+ v = v.replace('&', '&')
+ return k, v
+
+ def decode_entities(self, element, data):
+ data = data.replace('<', '<')
+ data = data.replace('<', '<')
+ data = data.replace('<', '<')
+ data = data.replace('>', '>')
+ data = data.replace('>', '>')
+ data = data.replace('>', '>')
+ data = data.replace('&', '&')
+ data = data.replace('&', '&')
+ data = data.replace('"', '"')
+ data = data.replace('"', '"')
+ data = data.replace(''', ''')
+ data = data.replace(''', ''')
+ 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
+
+ @staticmethod
+ def strattrs(attrs):
+ return ''.join(
+ ' %s="%s"' % (n, v.replace('"', '"'))
+ for n, v in attrs
+ )
diff --git a/vendor/feedparser/parsers/strict.py b/vendor/feedparser/parsers/strict.py
new file mode 100644
index 000000000..bc1297009
--- /dev/null
+++ b/vendor/feedparser/parsers/strict.py
@@ -0,0 +1,137 @@
+# The strict feed parser that interfaces with an XML parsing library
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, unicode_literals
+
+from ..exceptions import UndeclaredNamespace
+
+
+class _StrictFeedParser(object):
+ def __init__(self, baseuri, baselang, encoding):
+ self.bozo = 0
+ self.exc = None
+ self.decls = {}
+ self.baseuri = baseuri or ''
+ self.lang = baselang
+ self.encoding = encoding
+ super(_StrictFeedParser, self).__init__()
+
+ @staticmethod
+ def _normalize_attributes(kv):
+ k = kv[0].lower()
+ v = k in ('rel', 'type') and kv[1].lower() or kv[1]
+ return k, v
+
+ def startPrefixMapping(self, prefix, uri):
+ if not uri:
+ return
+ # Jython uses '' instead of None; standardize on None
+ prefix = prefix or None
+ self.track_namespace(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:
+ # match any backend.userland.com namespace
+ namespace = 'http://backend.userland.com/rss'
+ lowernamespace = namespace
+ if qname and qname.find(':') > 0:
+ givenprefix = qname.split(':')[0]
+ else:
+ givenprefix = None
+ prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
+ if givenprefix and (prefix is None or (prefix == '' and lowernamespace == '')) and givenprefix not in self.namespaces_in_use:
+ raise UndeclaredNamespace("'%s' is not associated with a namespace" % givenprefix)
+ localname = str(localname).lower()
+
+ # qname implementation is horribly broken in Python 2.1 (it
+ # doesn't report any), and slightly broken in Python 2.2 (it
+ # doesn't report the xml: namespace). So we match up namespaces
+ # with a known list first, and then possibly override them with
+ # the qnames the SAX parser gives us (if indeed it gives us any
+ # at all). Thanks to MatejC for helping me test this and
+ # tirelessly telling me that it didn't work yet.
+ attrsD, self.decls = self.decls, {}
+ if localname == 'math' and namespace == 'http://www.w3.org/1998/Math/MathML':
+ attrsD['xmlns'] = namespace
+ if localname == 'svg' and namespace == 'http://www.w3.org/2000/svg':
+ attrsD['xmlns'] = namespace
+
+ if prefix:
+ localname = prefix.lower() + ':' + localname
+ elif namespace and not qname: # Expat
+ for name, value in self.namespaces_in_use.items():
+ if name and value == namespace:
+ localname = name + ':' + localname
+ break
+
+ for (namespace, attrlocalname), attrvalue in attrs.items():
+ lowernamespace = (namespace or '').lower()
+ prefix = self._matchnamespaces.get(lowernamespace, '')
+ if prefix:
+ attrlocalname = prefix + ':' + attrlocalname
+ attrsD[str(attrlocalname).lower()] = attrvalue
+ for qname in attrs.getQNames():
+ attrsD[str(qname).lower()] = attrs.getValueByQName(qname)
+ localname = str(localname).lower()
+ self.unknown_starttag(localname, list(attrsD.items()))
+
+ def characters(self, text):
+ self.handle_data(text)
+
+ def endElementNS(self, name, qname):
+ namespace, localname = name
+ lowernamespace = str(namespace or '').lower()
+ if qname and qname.find(':') > 0:
+ givenprefix = qname.split(':')[0]
+ else:
+ givenprefix = ''
+ prefix = self._matchnamespaces.get(lowernamespace, givenprefix)
+ if prefix:
+ localname = prefix + ':' + localname
+ elif namespace and not qname: # Expat
+ for name, value in self.namespaces_in_use.items():
+ if name and value == namespace:
+ localname = name + ':' + localname
+ break
+ localname = str(localname).lower()
+ self.unknown_endtag(localname)
+
+ def error(self, exc):
+ self.bozo = 1
+ self.exc = exc
+
+ # drv_libxml2 calls warning() in some cases
+ warning = error
+
+ def fatalError(self, exc):
+ self.error(exc)
+ raise exc
diff --git a/vendor/feedparser/sanitizer.py b/vendor/feedparser/sanitizer.py
new file mode 100644
index 000000000..a04cba161
--- /dev/null
+++ b/vendor/feedparser/sanitizer.py
@@ -0,0 +1,955 @@
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import re
+
+from .html import _BaseHTMLProcessor
+from .sgml import _SGML_AVAILABLE
+from .urls import make_safe_absolute_uri
+
+
+class _HTMLSanitizer(_BaseHTMLProcessor):
+ acceptable_elements = {
+ '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',
+ 'dialog',
+ 'dir',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
+ 'event-source',
+ 'fieldset',
+ 'figcaption',
+ 'figure',
+ 'font',
+ 'footer',
+ 'form',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'header',
+ 'hr',
+ 'i',
+ 'img',
+ 'input',
+ 'ins',
+ 'kbd',
+ 'keygen',
+ 'label',
+ 'legend',
+ 'li',
+ 'm',
+ 'map',
+ 'menu',
+ 'meter',
+ 'multicol',
+ 'nav',
+ 'nextid',
+ 'noscript',
+ 'ol',
+ 'optgroup',
+ 'option',
+ 'output',
+ 'p',
+ 'pre',
+ 'progress',
+ 'q',
+ 's',
+ 'samp',
+ 'section',
+ 'select',
+ 'small',
+ 'sound',
+ 'source',
+ 'spacer',
+ 'span',
+ 'strike',
+ 'strong',
+ 'sub',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'textarea',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'time',
+ 'tr',
+ 'tt',
+ 'u',
+ 'ul',
+ 'var',
+ 'video',
+ }
+
+ acceptable_attributes = {
+ 'abbr',
+ 'accept',
+ 'accept-charset',
+ 'accesskey',
+ 'action',
+ 'align',
+ 'alt',
+ 'autocomplete',
+ 'autofocus',
+ 'axis',
+ 'background',
+ 'balance',
+ 'bgcolor',
+ 'bgproperties',
+ 'border',
+ 'bordercolor',
+ 'bordercolordark',
+ 'bordercolorlight',
+ 'bottompadding',
+ 'cellpadding',
+ 'cellspacing',
+ 'ch',
+ 'challenge',
+ 'char',
+ 'charoff',
+ 'charset',
+ 'checked',
+ 'choff',
+ 'cite',
+ 'class',
+ 'clear',
+ 'color',
+ 'cols',
+ 'colspan',
+ 'compact',
+ 'contenteditable',
+ 'controls',
+ 'coords',
+ 'data',
+ 'datafld',
+ 'datapagesize',
+ 'datasrc',
+ 'datetime',
+ 'default',
+ 'delay',
+ 'dir',
+ 'disabled',
+ 'draggable',
+ 'dynsrc',
+ 'enctype',
+ 'end',
+ 'face',
+ 'for',
+ 'form',
+ 'frame',
+ 'galleryimg',
+ 'gutter',
+ 'headers',
+ 'height',
+ 'hidden',
+ 'hidefocus',
+ 'high',
+ 'href',
+ 'hreflang',
+ 'hspace',
+ 'icon',
+ 'id',
+ 'inputmode',
+ 'ismap',
+ 'keytype',
+ 'label',
+ 'lang',
+ 'leftspacing',
+ 'list',
+ 'longdesc',
+ 'loop',
+ 'loopcount',
+ 'loopend',
+ 'loopstart',
+ 'low',
+ 'lowsrc',
+ 'max',
+ 'maxlength',
+ 'media',
+ 'method',
+ 'min',
+ 'multiple',
+ 'name',
+ 'nohref',
+ 'noshade',
+ 'nowrap',
+ 'open',
+ 'optimum',
+ 'pattern',
+ 'ping',
+ 'point-size',
+ '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',
+ 'urn',
+ 'usemap',
+ 'valign',
+ 'value',
+ 'variable',
+ 'volume',
+ 'vrml',
+ 'vspace',
+ 'width',
+ 'wrap',
+ 'xml:lang',
+ }
+
+ unacceptable_elements_with_end_tag = {
+ 'applet',
+ 'script',
+ 'style',
+ }
+
+ acceptable_css_properties = {
+ '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',
+ 'font-family',
+ 'font-size',
+ 'font-style',
+ 'font-variant',
+ 'font-weight',
+ 'height',
+ 'letter-spacing',
+ 'line-height',
+ 'overflow',
+ 'pause',
+ 'pause-after',
+ 'pause-before',
+ 'pitch',
+ 'pitch-range',
+ 'richness',
+ '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',
+ }
+
+ # survey of common keywords found in feeds
+ acceptable_css_keywords = {
+ '!important',
+ 'aqua',
+ 'auto',
+ 'black',
+ 'block',
+ 'blue',
+ 'bold',
+ 'both',
+ 'bottom',
+ 'brown',
+ 'center',
+ 'collapse',
+ 'dashed',
+ 'dotted',
+ 'fuchsia',
+ 'gray',
+ 'green',
+ 'italic',
+ 'left',
+ 'lime',
+ 'maroon',
+ 'medium',
+ 'navy',
+ 'none',
+ 'normal',
+ 'nowrap',
+ 'olive',
+ 'pointer',
+ 'purple',
+ 'red',
+ 'right',
+ 'silver',
+ 'solid',
+ 'teal',
+ 'top',
+ 'transparent',
+ 'underline',
+ 'white',
+ 'yellow',
+ }
+
+ valid_css_values = re.compile(
+ r'^('
+ r'#[0-9a-f]+' # Hex values
+ r'|rgb\(\d+%?,\d*%?,?\d*%?\)?' # RGB values
+ r'|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?' # Sizes/widths
+ r')$'
+ )
+
+ mathml_elements = {
+ '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 = {
+ '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',
+ 'animateTransform',
+ 'circle',
+ 'defs',
+ 'desc',
+ 'ellipse',
+ 'font-face',
+ 'font-face-name',
+ 'font-face-src',
+ 'foreignObject',
+ 'g',
+ 'glyph',
+ 'hkern',
+ 'line',
+ 'linearGradient',
+ 'marker',
+ 'metadata',
+ 'missing-glyph',
+ 'mpath',
+ 'path',
+ 'polygon',
+ 'polyline',
+ 'radialGradient',
+ 'rect',
+ 'set',
+ 'stop',
+ 'svg',
+ 'switch',
+ 'text',
+ 'title',
+ 'tspan',
+ 'use',
+ }
+
+ # svgtiny + class + opacity + offset + xmlns + xmlns:xlink
+ svg_attributes = {
+ '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',
+ 'descent',
+ 'display',
+ 'dur',
+ 'dx',
+ 'dy',
+ 'end',
+ 'fill',
+ 'fill-opacity',
+ 'fill-rule',
+ 'font-family',
+ 'font-size',
+ 'font-stretch',
+ 'font-style',
+ 'font-variant',
+ 'font-weight',
+ 'from',
+ 'fx',
+ 'fy',
+ 'g1',
+ 'g2',
+ 'glyph-name',
+ 'gradientUnits',
+ 'hanging',
+ 'height',
+ 'horiz-adv-x',
+ 'horiz-origin-x',
+ 'id',
+ 'ideographic',
+ 'k',
+ 'keyPoints',
+ 'keySplines',
+ 'keyTimes',
+ 'lang',
+ 'marker-end',
+ 'marker-mid',
+ 'marker-start',
+ 'markerHeight',
+ 'markerUnits',
+ 'markerWidth',
+ 'mathematical',
+ 'max',
+ 'min',
+ 'name',
+ 'offset',
+ 'opacity',
+ 'orient',
+ 'origin',
+ 'overline-position',
+ 'overline-thickness',
+ 'panose-1',
+ 'path',
+ 'pathLength',
+ 'points',
+ 'preserveAspectRatio',
+ 'r',
+ 'refX',
+ 'refY',
+ 'repeatCount',
+ 'repeatDur',
+ 'requiredExtensions',
+ 'requiredFeatures',
+ 'restart',
+ 'rotate',
+ 'rx',
+ 'ry',
+ 'slope',
+ 'stemh',
+ 'stemv',
+ 'stop-color',
+ 'stop-opacity',
+ 'strikethrough-position',
+ 'strikethrough-thickness',
+ 'stroke',
+ 'stroke-dasharray',
+ 'stroke-dashoffset',
+ 'stroke-linecap',
+ 'stroke-linejoin',
+ 'stroke-miterlimit',
+ 'stroke-opacity',
+ 'stroke-width',
+ 'systemLanguage',
+ 'target',
+ 'text-anchor',
+ 'to',
+ 'transform',
+ 'type',
+ 'u1',
+ 'u2',
+ 'underline-position',
+ 'underline-thickness',
+ 'unicode',
+ 'unicode-range',
+ 'units-per-em',
+ 'values',
+ 'version',
+ 'viewBox',
+ 'visibility',
+ 'width',
+ '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',
+ }
+
+ svg_attr_map = None
+ svg_elem_map = None
+
+ acceptable_svg_properties = {
+ 'fill',
+ 'fill-opacity',
+ 'fill-rule',
+ 'stroke',
+ 'stroke-linecap',
+ 'stroke-linejoin',
+ 'stroke-opacity',
+ 'stroke-width',
+ }
+
+ def __init__(self, encoding=None, _type='application/xhtml+xml'):
+ super(_HTMLSanitizer, self).__init__(encoding, _type)
+
+ self.unacceptablestack = 0
+ self.mathmlOK = 0
+ self.svgOK = 0
+
+ def reset(self):
+ super(_HTMLSanitizer, self).reset()
+ self.unacceptablestack = 0
+ self.mathmlOK = 0
+ self.svgOK = 0
+
+ def unknown_starttag(self, tag, attrs):
+ acceptable_attributes = self.acceptable_attributes
+ keymap = {}
+ if tag not in self.acceptable_elements or self.svgOK:
+ if tag in self.unacceptable_elements_with_end_tag:
+ self.unacceptablestack += 1
+
+ # add implicit namespaces to html5 inline svg/mathml
+ if self._type.endswith('html'):
+ if not dict(attrs).get('xmlns'):
+ if tag == 'svg':
+ attrs.append(('xmlns', 'http://www.w3.org/2000/svg'))
+ if tag == 'math':
+ attrs.append(('xmlns', 'http://www.w3.org/1998/Math/MathML'))
+
+ # not otherwise acceptable, perhaps it is MathML or SVG?
+ if tag == 'math' and ('xmlns', 'http://www.w3.org/1998/Math/MathML') in attrs:
+ self.mathmlOK += 1
+ if tag == 'svg' and ('xmlns', 'http://www.w3.org/2000/svg') in attrs:
+ self.svgOK += 1
+
+ # chose acceptable attributes based on tag class, else bail
+ if self.mathmlOK and tag in self.mathml_elements:
+ acceptable_attributes = self.mathml_attributes
+ elif self.svgOK and tag in self.svg_elements:
+ # For most vocabularies, lowercasing is a good idea. Many
+ # svg elements, however, are camel case.
+ if not self.svg_attr_map:
+ lower = [attr.lower() for attr in self.svg_attributes]
+ mix = [a for a in self.svg_attributes if a not in lower]
+ self.svg_attributes = lower
+ self.svg_attr_map = {a.lower(): a for a in mix}
+
+ lower = [attr.lower() for attr in self.svg_elements]
+ mix = [a for a in self.svg_elements if a not in lower]
+ self.svg_elements = lower
+ self.svg_elem_map = {a.lower(): a for a in mix}
+ acceptable_attributes = self.svg_attributes
+ tag = self.svg_elem_map.get(tag, tag)
+ keymap = self.svg_attr_map
+ elif tag not in self.acceptable_elements:
+ return
+
+ # declare xlink namespace, if needed
+ if self.mathmlOK or self.svgOK:
+ if any((a for a in attrs if a[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'))
+
+ clean_attrs = []
+ for key, value in self.normalize_attrs(attrs):
+ if key in acceptable_attributes:
+ key = keymap.get(key, key)
+ # make sure the uri uses an acceptable uri scheme
+ if key == 'href':
+ value = make_safe_absolute_uri(value)
+ clean_attrs.append((key, value))
+ elif key == 'style':
+ clean_value = self.sanitize_style(value)
+ if clean_value:
+ clean_attrs.append((key, clean_value))
+ super(_HTMLSanitizer, self).unknown_starttag(tag, clean_attrs)
+
+ def unknown_endtag(self, tag):
+ if tag not in self.acceptable_elements:
+ 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
+ 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
+ else:
+ return
+ super(_HTMLSanitizer, self).unknown_endtag(tag)
+
+ def handle_pi(self, text):
+ pass
+
+ def handle_decl(self, text):
+ pass
+
+ def handle_data(self, text):
+ if not self.unacceptablestack:
+ super(_HTMLSanitizer, self).handle_data(text)
+
+ def sanitize_style(self, style):
+ # disallow urls
+ style = re.compile(r'url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style)
+
+ # gauntlet
+ if not re.match(r"""^([:,;#%.\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(r"\s*[-\w]+\s*:\s*[^:;]*;?", '', style).strip():
+ return ''
+
+ clean = []
+ for prop, value in re.findall(r"([-\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 (
+ keyword not 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)
+
+ def parse_comment(self, i, report=1):
+ ret = super(_HTMLSanitizer, self).parse_comment(i, report)
+ if ret >= 0:
+ return ret
+ # if ret == -1, this may be a malicious attempt to circumvent
+ # sanitization, or a page-destroying unclosed comment
+ match = re.compile(r'--[^>]*>').search(self.rawdata, i+4)
+ if match:
+ return match.end()
+ # unclosed comment; deliberately fail to handle_data()
+ return len(self.rawdata)
+
+
+def _sanitize_html(html_source, encoding, _type):
+ if not _SGML_AVAILABLE:
+ return html_source
+ p = _HTMLSanitizer(encoding, _type)
+ html_source = html_source.replace('
+RE_ENTITY_PATTERN = re.compile(br'^\s*]*?)>', re.MULTILINE)
+
+# Match XML DOCTYPE declarations.
+# Example:
+RE_DOCTYPE_PATTERN = re.compile(br'^\s*]*?)>', re.MULTILINE)
+
+# Match safe entity declarations.
+# This will allow hexadecimal character references through,
+# as well as text, but not arbitrary nested entities.
+# Example: cubed "³"
+# Example: copyright "(C)"
+# Forbidden: explode1 "&explode2;&explode2;"
+RE_SAFE_ENTITY_PATTERN = re.compile(br'\s+(\w+)\s+"(\w+;|[^&"]*)"')
+
+
+def replace_doctype(data):
+ """Strips and replaces the DOCTYPE, returns (rss_version, stripped_data)
+
+ rss_version may be 'rss091n' or None
+ stripped_data is the same XML document with a replaced DOCTYPE
+ """
+
+ # Divide the document into two groups by finding the location
+ # of the first element that doesn't begin with '' or '\n\n]>'
+ data = RE_DOCTYPE_PATTERN.sub(replacement, head) + data
+
+ # Precompute the safe entities for the loose parser.
+ safe_entities = {
+ k.decode('utf-8'): v.decode('utf-8')
+ for k, v in RE_SAFE_ENTITY_PATTERN.findall(replacement)
+ }
+ return version, data, safe_entities
diff --git a/vendor/feedparser/sgml.py b/vendor/feedparser/sgml.py
new file mode 100644
index 000000000..fcc540a8b
--- /dev/null
+++ b/vendor/feedparser/sgml.py
@@ -0,0 +1,136 @@
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+
+import re
+
+__all__ = [
+ '_SGML_AVAILABLE',
+ 'sgmllib',
+ 'charref',
+ 'tagfind',
+ 'attrfind',
+ 'entityref',
+ 'incomplete',
+ 'interesting',
+ 'shorttag',
+ 'shorttagopen',
+ 'starttagopen',
+ 'endbracket',
+]
+
+# sgmllib is not available by default in Python 3; if the end user doesn't have
+# it available then we'll lose illformed XML parsing and content sanitizing
+try:
+ import sgmllib
+except ImportError:
+ # This is probably Python 3, which doesn't include sgmllib anymore
+ _SGML_AVAILABLE = 0
+
+ # Mock sgmllib enough to allow subclassing later on
+ class sgmllib(object):
+ SGMLParseError = EnvironmentError
+
+ class SGMLParser(object):
+ lasttag = None
+ rawdata = None
+
+ def close(self):
+ pass
+
+ def feed(self, data):
+ pass
+
+ def goahead(self, i):
+ pass
+
+ def parse_declaration(self, i):
+ pass
+
+ def parse_starttag(self, i):
+ pass
+
+ def reset(self):
+ pass
+
+else:
+ _SGML_AVAILABLE = 1
+
+ # sgmllib defines a number of module-level regular expressions that are
+ # insufficient for the XML parsing feedparser needs. Rather than modify
+ # the variables directly in sgmllib, they're defined here using the same
+ # names, and the compiled code objects of several sgmllib.SGMLParser
+ # methods are copied into _BaseHTMLProcessor so that they execute in
+ # feedparser's scope instead of sgmllib's scope.
+ charref = re.compile(r'(\d+|[xX][0-9a-fA-F]+);')
+ tagfind = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*')
+ attrfind = re.compile(
+ r"""\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)[$]?(\s*=\s*"""
+ r"""('[^']*'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$()_#=~'"@]*))?"""
+ )
+
+ # Unfortunately, these must be copied over to prevent NameError exceptions
+ entityref = sgmllib.entityref
+ incomplete = sgmllib.incomplete
+ interesting = sgmllib.interesting
+ shorttag = sgmllib.shorttag
+ shorttagopen = sgmllib.shorttagopen
+ starttagopen = sgmllib.starttagopen
+
+
+ class _EndBracketRegEx:
+ def __init__(self):
+ # Overriding the built-in sgmllib.endbracket regex allows the
+ # parser to find angle brackets embedded in element attributes.
+ self.endbracket = re.compile(
+ r'('
+ r"""[^'"<>]"""
+ r"""|"[^"]*"(?=>|/|\s|\w+=)"""
+ r"""|'[^']*'(?=>|/|\s|\w+=))*(?=[<>])"""
+ r"""|.*?(?=[<>]"""
+ r')'
+ )
+
+ def search(self, target, index=0):
+ match = self.endbracket.match(target, index)
+ if match is not None:
+ # Returning a new object in the calling thread's context
+ # resolves a thread-safety.
+ return EndBracketMatch(match)
+ return None
+
+
+ class EndBracketMatch:
+ def __init__(self, match):
+ self.match = match
+
+ def start(self, n):
+ return self.match.end(n)
+
+
+ endbracket = _EndBracketRegEx()
diff --git a/vendor/feedparser/urls.py b/vendor/feedparser/urls.py
new file mode 100644
index 000000000..191447fe3
--- /dev/null
+++ b/vendor/feedparser/urls.py
@@ -0,0 +1,162 @@
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import re
+
+try:
+ import urllib.parse as urlparse
+except ImportError:
+ import urlparse as urlparse
+
+from .html import _BaseHTMLProcessor
+
+# If you want feedparser to allow all URL schemes, set this to ()
+# List culled from Python's urlparse documentation at:
+# http://docs.python.org/library/urlparse.html
+# as well as from "URI scheme" at Wikipedia:
+# https://secure.wikimedia.org/wikipedia/en/wiki/URI_scheme
+# Many more will likely need to be added!
+ACCEPTABLE_URI_SCHEMES = (
+ 'file', 'ftp', 'gopher', 'h323', 'hdl', 'http', 'https', 'imap', 'magnet',
+ 'mailto', 'mms', 'news', 'nntp', 'prospero', 'rsync', 'rtsp', 'rtspu',
+ 'sftp', 'shttp', 'sip', 'sips', 'snews', 'svn', 'svn+ssh', 'telnet',
+ 'wais',
+ # Additional common-but-unofficial schemes
+ 'aim', 'callto', 'cvs', 'facetime', 'feed', 'git', 'gtalk', 'irc', 'ircs',
+ 'irc6', 'itms', 'mms', 'msnim', 'skype', 'ssh', 'smb', 'svn', 'ymsg',
+)
+
+_urifixer = re.compile('^([A-Za-z][A-Za-z0-9+-.]*://)(/*)(.*?)')
+
+
+def _urljoin(base, uri):
+ uri = _urifixer.sub(r'\1\3', uri)
+ try:
+ uri = urlparse.urljoin(base, uri)
+ except ValueError:
+ uri = ''
+ return uri
+
+
+def convert_to_idn(url):
+ """Convert a URL to IDN notation"""
+ # this function should only be called with a unicode string
+ # strategy: if the host cannot be encoded in ascii, then
+ # it'll be necessary to encode it in idn form
+ parts = list(urlparse.urlsplit(url))
+ try:
+ parts[1].encode('ascii')
+ except UnicodeEncodeError:
+ # the url needs to be converted to idn notation
+ host = parts[1].rsplit(':', 1)
+ newhost = []
+ port = ''
+ if len(host) == 2:
+ port = host.pop()
+ for h in host[0].split('.'):
+ newhost.append(h.encode('idna').decode('utf-8'))
+ parts[1] = '.'.join(newhost)
+ if port:
+ parts[1] += ':' + port
+ return urlparse.urlunsplit(parts)
+ else:
+ return url
+
+
+def make_safe_absolute_uri(base, rel=None):
+ # bail if ACCEPTABLE_URI_SCHEMES is empty
+ if not ACCEPTABLE_URI_SCHEMES:
+ return _urljoin(base, rel or '')
+ if not base:
+ return rel or ''
+ if not rel:
+ try:
+ scheme = urlparse.urlparse(base)[0]
+ except ValueError:
+ return ''
+ if not scheme or scheme in ACCEPTABLE_URI_SCHEMES:
+ return base
+ return ''
+ uri = _urljoin(base, rel)
+ if uri.strip().split(':', 1)[0] not in ACCEPTABLE_URI_SCHEMES:
+ return ''
+ return uri
+
+
+class RelativeURIResolver(_BaseHTMLProcessor):
+ relative_uris = {
+ ('a', 'href'),
+ ('applet', 'codebase'),
+ ('area', 'href'),
+ ('audio', 'src'),
+ ('blockquote', 'cite'),
+ ('body', 'background'),
+ ('del', 'cite'),
+ ('form', 'action'),
+ ('frame', 'longdesc'),
+ ('frame', 'src'),
+ ('iframe', 'longdesc'),
+ ('iframe', 'src'),
+ ('head', 'profile'),
+ ('img', 'longdesc'),
+ ('img', 'src'),
+ ('img', 'usemap'),
+ ('input', 'src'),
+ ('input', 'usemap'),
+ ('ins', 'cite'),
+ ('link', 'href'),
+ ('object', 'classid'),
+ ('object', 'codebase'),
+ ('object', 'data'),
+ ('object', 'usemap'),
+ ('q', 'cite'),
+ ('script', 'src'),
+ ('source', 'src'),
+ ('video', 'poster'),
+ ('video', 'src'),
+ }
+
+ def __init__(self, baseuri, encoding, _type):
+ _BaseHTMLProcessor.__init__(self, encoding, _type)
+ self.baseuri = baseuri
+
+ def resolve_uri(self, uri):
+ return make_safe_absolute_uri(self.baseuri, uri.strip())
+
+ def unknown_starttag(self, tag, attrs):
+ attrs = self.normalize_attrs(attrs)
+ attrs = [(key, ((tag, key) in self.relative_uris) and self.resolve_uri(value) or value) for key, value in attrs]
+ super(RelativeURIResolver, self).unknown_starttag(tag, attrs)
+
+
+def resolve_relative_uris(html_source, base_uri, encoding, type_):
+ p = RelativeURIResolver(base_uri, encoding, type_)
+ p.feed(html_source)
+ return p.output()
diff --git a/vendor/feedparser/util.py b/vendor/feedparser/util.py
new file mode 100644
index 000000000..ebe4b9df1
--- /dev/null
+++ b/vendor/feedparser/util.py
@@ -0,0 +1,166 @@
+# Copyright 2010-2020 Kurt McKee
+# Copyright 2002-2008 Mark Pilgrim
+# All rights reserved.
+#
+# This file is a part of feedparser.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import warnings
+
+
+class FeedParserDict(dict):
+ keymap = {
+ 'channel': 'feed',
+ 'items': 'entries',
+ 'guid': 'id',
+ 'date': 'updated',
+ 'date_parsed': 'updated_parsed',
+ 'description': ['summary', 'subtitle'],
+ 'description_detail': ['summary_detail', 'subtitle_detail'],
+ 'url': ['href'],
+ 'modified': 'updated',
+ 'modified_parsed': 'updated_parsed',
+ 'issued': 'published',
+ 'issued_parsed': 'published_parsed',
+ 'copyright': 'rights',
+ 'copyright_detail': 'rights_detail',
+ 'tagline': 'subtitle',
+ 'tagline_detail': 'subtitle_detail',
+ }
+
+ def __getitem__(self, key):
+ """
+ :return: A :class:`FeedParserDict`.
+ """
+
+ if key == 'category':
+ try:
+ return dict.__getitem__(self, 'tags')[0]['term']
+ except IndexError:
+ raise KeyError("object doesn't have key 'category'")
+ elif key == 'enclosures':
+ norel = lambda link: FeedParserDict([(name, value) for (name, value) in link.items() if name != 'rel'])
+ return [
+ norel(link)
+ for link in dict.__getitem__(self, 'links')
+ if link['rel'] == 'enclosure'
+ ]
+ elif key == 'license':
+ for link in dict.__getitem__(self, 'links'):
+ if link['rel'] == 'license' and 'href' in link:
+ return link['href']
+ elif key == 'updated':
+ # Temporarily help developers out by keeping the old
+ # broken behavior that was reported in issue 310.
+ # This fix was proposed in issue 328.
+ if (
+ not dict.__contains__(self, 'updated')
+ and dict.__contains__(self, 'published')
+ ):
+ warnings.warn(
+ "To avoid breaking existing software while "
+ "fixing issue 310, a temporary mapping has been created "
+ "from `updated` to `published` if `updated` doesn't "
+ "exist. This fallback will be removed in a future version "
+ "of feedparser.",
+ DeprecationWarning,
+ )
+ return dict.__getitem__(self, 'published')
+ return dict.__getitem__(self, 'updated')
+ elif key == 'updated_parsed':
+ if (
+ not dict.__contains__(self, 'updated_parsed')
+ and dict.__contains__(self, 'published_parsed')
+ ):
+ warnings.warn(
+ "To avoid breaking existing software while "
+ "fixing issue 310, a temporary mapping has been created "
+ "from `updated_parsed` to `published_parsed` if "
+ "`updated_parsed` doesn't exist. This fallback will be "
+ "removed in a future version of feedparser.",
+ DeprecationWarning,
+ )
+ return dict.__getitem__(self, 'published_parsed')
+ return dict.__getitem__(self, 'updated_parsed')
+ else:
+ realkey = self.keymap.get(key, key)
+ if isinstance(realkey, list):
+ for k in realkey:
+ if dict.__contains__(self, k):
+ return dict.__getitem__(self, k)
+ elif dict.__contains__(self, realkey):
+ return dict.__getitem__(self, realkey)
+ return dict.__getitem__(self, key)
+
+ def __contains__(self, key):
+ if key in ('updated', 'updated_parsed'):
+ # Temporarily help developers out by keeping the old
+ # broken behavior that was reported in issue 310.
+ # This fix was proposed in issue 328.
+ return dict.__contains__(self, key)
+ try:
+ self.__getitem__(key)
+ except KeyError:
+ return False
+ else:
+ return True
+
+ has_key = __contains__
+
+ def get(self, key, default=None):
+ """
+ :return: A :class:`FeedParserDict`.
+ """
+
+ try:
+ return self.__getitem__(key)
+ except KeyError:
+ return default
+
+ def __setitem__(self, key, value):
+ key = self.keymap.get(key, key)
+ if isinstance(key, list):
+ key = key[0]
+ return dict.__setitem__(self, key, value)
+
+ def setdefault(self, k, default):
+ if k not in self:
+ self[k] = default
+ return default
+ return self[k]
+
+ def __getattr__(self, key):
+ # __getattribute__() is called first; this will be called
+ # only if an attribute was not already found
+ try:
+ return self.__getitem__(key)
+ except KeyError:
+ raise AttributeError("object has no attribute '%s'" % key)
+
+ def __hash__(self):
+ # This is incorrect behavior -- dictionaries shouldn't be hashable.
+ # Note to self: remove this behavior in the future.
+ return id(self)