SeriesPlugin 1.0: First public version
[enigma2-plugins.git] / seriesplugin / src / identifiers / bs4 / element.py
1 import collections
2 import re
3 import sys
4 import warnings
5 from bs4.dammit import EntitySubstitution
6
7 DEFAULT_OUTPUT_ENCODING = "utf-8"
8 PY3K = (sys.version_info[0] > 2)
9
10 whitespace_re = re.compile("\s+")
11
12 def _alias(attr):
13     """Alias one attribute name to another for backward compatibility"""
14     @property
15     def alias(self):
16         return getattr(self, attr)
17
18     @alias.setter
19     def alias(self):
20         return setattr(self, attr)
21     return alias
22
23
24 class NamespacedAttribute(unicode):
25
26     def __new__(cls, prefix, name, namespace=None):
27         if name is None:
28             obj = unicode.__new__(cls, prefix)
29         else:
30             obj = unicode.__new__(cls, prefix + ":" + name)
31         obj.prefix = prefix
32         obj.name = name
33         obj.namespace = namespace
34         return obj
35
36 class AttributeValueWithCharsetSubstitution(unicode):
37     """A stand-in object for a character encoding specified in HTML."""
38
39 class CharsetMetaAttributeValue(AttributeValueWithCharsetSubstitution):
40     """A generic stand-in for the value of a meta tag's 'charset' attribute.
41
42     When Beautiful Soup parses the markup '<meta charset="utf8">', the
43     value of the 'charset' attribute will be one of these objects.
44     """
45
46     def __new__(cls, original_value):
47         obj = unicode.__new__(cls, original_value)
48         obj.original_value = original_value
49         return obj
50
51     def encode(self, encoding):
52         return encoding
53
54
55 class ContentMetaAttributeValue(AttributeValueWithCharsetSubstitution):
56     """A generic stand-in for the value of a meta tag's 'content' attribute.
57
58     When Beautiful Soup parses the markup:
59      <meta http-equiv="content-type" content="text/html; charset=utf8">
60
61     The value of the 'content' attribute will be one of these objects.
62     """
63
64     CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M)
65
66     def __new__(cls, original_value):
67         match = cls.CHARSET_RE.search(original_value)
68         if match is None:
69             # No substitution necessary.
70             return unicode.__new__(unicode, original_value)
71
72         obj = unicode.__new__(cls, original_value)
73         obj.original_value = original_value
74         return obj
75
76     def encode(self, encoding):
77         def rewrite(match):
78             return match.group(1) + encoding
79         return self.CHARSET_RE.sub(rewrite, self.original_value)
80
81
82 class PageElement(object):
83     """Contains the navigational information for some part of the page
84     (either a tag or a piece of text)"""
85
86     # There are five possible values for the "formatter" argument passed in
87     # to methods like encode() and prettify():
88     #
89     # "html" - All Unicode characters with corresponding HTML entities
90     #   are converted to those entities on output.
91     # "minimal" - Bare ampersands and angle brackets are converted to
92     #   XML entities: &amp; &lt; &gt;
93     # None - The null formatter. Unicode characters are never
94     #   converted to entities.  This is not recommended, but it's
95     #   faster than "minimal".
96     # A function - This function will be called on every string that
97     #  needs to undergo entity substition
98     FORMATTERS = {
99         "html" : EntitySubstitution.substitute_html,
100         "minimal" : EntitySubstitution.substitute_xml,
101         None : None
102         }
103
104     @classmethod
105     def format_string(self, s, formatter='minimal'):
106         """Format the given string using the given formatter."""
107         if not callable(formatter):
108             formatter = self.FORMATTERS.get(
109                 formatter, EntitySubstitution.substitute_xml)
110         if formatter is None:
111             output = s
112         else:
113             output = formatter(s)
114         return output
115
116     def setup(self, parent=None, previous_element=None):
117         """Sets up the initial relations between this element and
118         other elements."""
119         self.parent = parent
120         self.previous_element = previous_element
121         if previous_element is not None:
122             self.previous_element.next_element = self
123         self.next_element = None
124         self.previous_sibling = None
125         self.next_sibling = None
126         if self.parent is not None and self.parent.contents:
127             self.previous_sibling = self.parent.contents[-1]
128             self.previous_sibling.next_sibling = self
129
130     nextSibling = _alias("next_sibling")  # BS3
131     previousSibling = _alias("previous_sibling")  # BS3
132
133     def replace_with(self, replace_with):
134         if replace_with is self:
135             return
136         if replace_with is self.parent:
137             raise ValueError("Cannot replace a Tag with its parent.")
138         old_parent = self.parent
139         my_index = self.parent.index(self)
140         self.extract()
141         old_parent.insert(my_index, replace_with)
142         return self
143     replaceWith = replace_with  # BS3
144
145     def unwrap(self):
146         my_parent = self.parent
147         my_index = self.parent.index(self)
148         self.extract()
149         for child in reversed(self.contents[:]):
150             my_parent.insert(my_index, child)
151         return self
152     replace_with_children = unwrap
153     replaceWithChildren = unwrap  # BS3
154
155     def wrap(self, wrap_inside):
156         me = self.replace_with(wrap_inside)
157         wrap_inside.append(me)
158         return wrap_inside
159
160     def extract(self):
161         """Destructively rips this element out of the tree."""
162         if self.parent is not None:
163             del self.parent.contents[self.parent.index(self)]
164
165         #Find the two elements that would be next to each other if
166         #this element (and any children) hadn't been parsed. Connect
167         #the two.
168         last_child = self._last_descendant()
169         next_element = last_child.next_element
170
171         if self.previous_element is not None:
172             self.previous_element.next_element = next_element
173         if next_element is not None:
174             next_element.previous_element = self.previous_element
175         self.previous_element = None
176         last_child.next_element = None
177
178         self.parent = None
179         if self.previous_sibling is not None:
180             self.previous_sibling.next_sibling = self.next_sibling
181         if self.next_sibling is not None:
182             self.next_sibling.previous_sibling = self.previous_sibling
183         self.previous_sibling = self.next_sibling = None
184         return self
185
186     def _last_descendant(self):
187         "Finds the last element beneath this object to be parsed."
188         last_child = self
189         while hasattr(last_child, 'contents') and last_child.contents:
190             last_child = last_child.contents[-1]
191         return last_child
192     # BS3: Not part of the API!
193     _lastRecursiveChild = _last_descendant
194
195     def insert(self, position, new_child):
196         if new_child is self:
197             raise ValueError("Cannot insert a tag into itself.")
198         if (isinstance(new_child, basestring)
199             and not isinstance(new_child, NavigableString)):
200             new_child = NavigableString(new_child)
201
202         position = min(position, len(self.contents))
203         if hasattr(new_child, 'parent') and new_child.parent is not None:
204             # We're 'inserting' an element that's already one
205             # of this object's children.
206             if new_child.parent is self:
207                 current_index = self.index(new_child)
208                 if current_index < position:
209                     # We're moving this element further down the list
210                     # of this object's children. That means that when
211                     # we extract this element, our target index will
212                     # jump down one.
213                     position -= 1
214             new_child.extract()
215
216         new_child.parent = self
217         previous_child = None
218         if position == 0:
219             new_child.previous_sibling = None
220             new_child.previous_element = self
221         else:
222             previous_child = self.contents[position - 1]
223             new_child.previous_sibling = previous_child
224             new_child.previous_sibling.next_sibling = new_child
225             new_child.previous_element = previous_child._last_descendant()
226         if new_child.previous_element is not None:
227             new_child.previous_element.next_element = new_child
228
229         new_childs_last_element = new_child._last_descendant()
230
231         if position >= len(self.contents):
232             new_child.next_sibling = None
233
234             parent = self
235             parents_next_sibling = None
236             while parents_next_sibling is None and parent is not None:
237                 parents_next_sibling = parent.next_sibling
238                 parent = parent.parent
239                 if parents_next_sibling is not None:
240                     # We found the element that comes next in the document.
241                     break
242             if parents_next_sibling is not None:
243                 new_childs_last_element.next_element = parents_next_sibling
244             else:
245                 # The last element of this tag is the last element in
246                 # the document.
247                 new_childs_last_element.next_element = None
248         else:
249             next_child = self.contents[position]
250             new_child.next_sibling = next_child
251             if new_child.next_sibling is not None:
252                 new_child.next_sibling.previous_sibling = new_child
253             new_childs_last_element.next_element = next_child
254
255         if new_childs_last_element.next_element is not None:
256             new_childs_last_element.next_element.previous_element = new_childs_last_element
257         self.contents.insert(position, new_child)
258
259     def append(self, tag):
260         """Appends the given tag to the contents of this tag."""
261         self.insert(len(self.contents), tag)
262
263     def insert_before(self, predecessor):
264         """Makes the given element the immediate predecessor of this one.
265
266         The two elements will have the same parent, and the given element
267         will be immediately before this one.
268         """
269         if self is predecessor:
270             raise ValueError("Can't insert an element before itself.")
271         parent = self.parent
272         if parent is None:
273             raise ValueError(
274                 "Element has no parent, so 'before' has no meaning.")
275         # Extract first so that the index won't be screwed up if they
276         # are siblings.
277         if isinstance(predecessor, PageElement):
278             predecessor.extract()
279         index = parent.index(self)
280         parent.insert(index, predecessor)
281
282     def insert_after(self, successor):
283         """Makes the given element the immediate successor of this one.
284
285         The two elements will have the same parent, and the given element
286         will be immediately after this one.
287         """
288         if self is successor:
289             raise ValueError("Can't insert an element after itself.")
290         parent = self.parent
291         if parent is None:
292             raise ValueError(
293                 "Element has no parent, so 'after' has no meaning.")
294         # Extract first so that the index won't be screwed up if they
295         # are siblings.
296         if isinstance(successor, PageElement):
297             successor.extract()
298         index = parent.index(self)
299         parent.insert(index+1, successor)
300
301     def find_next(self, name=None, attrs={}, text=None, **kwargs):
302         """Returns the first item that matches the given criteria and
303         appears after this Tag in the document."""
304         return self._find_one(self.find_all_next, name, attrs, text, **kwargs)
305     findNext = find_next  # BS3
306
307     def find_all_next(self, name=None, attrs={}, text=None, limit=None,
308                     **kwargs):
309         """Returns all items that match the given criteria and appear
310         after this Tag in the document."""
311         return self._find_all(name, attrs, text, limit, self.next_elements,
312                              **kwargs)
313     findAllNext = find_all_next  # BS3
314
315     def find_next_sibling(self, name=None, attrs={}, text=None, **kwargs):
316         """Returns the closest sibling to this Tag that matches the
317         given criteria and appears after this Tag in the document."""
318         return self._find_one(self.find_next_siblings, name, attrs, text,
319                              **kwargs)
320     findNextSibling = find_next_sibling  # BS3
321
322     def find_next_siblings(self, name=None, attrs={}, text=None, limit=None,
323                            **kwargs):
324         """Returns the siblings of this Tag that match the given
325         criteria and appear after this Tag in the document."""
326         return self._find_all(name, attrs, text, limit,
327                               self.next_siblings, **kwargs)
328     findNextSiblings = find_next_siblings   # BS3
329     fetchNextSiblings = find_next_siblings  # BS2
330
331     def find_previous(self, name=None, attrs={}, text=None, **kwargs):
332         """Returns the first item that matches the given criteria and
333         appears before this Tag in the document."""
334         return self._find_one(
335             self.find_all_previous, name, attrs, text, **kwargs)
336     findPrevious = find_previous  # BS3
337
338     def find_all_previous(self, name=None, attrs={}, text=None, limit=None,
339                         **kwargs):
340         """Returns all items that match the given criteria and appear
341         before this Tag in the document."""
342         return self._find_all(name, attrs, text, limit, self.previous_elements,
343                            **kwargs)
344     findAllPrevious = find_all_previous  # BS3
345     fetchPrevious = find_all_previous    # BS2
346
347     def find_previous_sibling(self, name=None, attrs={}, text=None, **kwargs):
348         """Returns the closest sibling to this Tag that matches the
349         given criteria and appears before this Tag in the document."""
350         return self._find_one(self.find_previous_siblings, name, attrs, text,
351                              **kwargs)
352     findPreviousSibling = find_previous_sibling  # BS3
353
354     def find_previous_siblings(self, name=None, attrs={}, text=None,
355                                limit=None, **kwargs):
356         """Returns the siblings of this Tag that match the given
357         criteria and appear before this Tag in the document."""
358         return self._find_all(name, attrs, text, limit,
359                               self.previous_siblings, **kwargs)
360     findPreviousSiblings = find_previous_siblings   # BS3
361     fetchPreviousSiblings = find_previous_siblings  # BS2
362
363     def find_parent(self, name=None, attrs={}, **kwargs):
364         """Returns the closest parent of this Tag that matches the given
365         criteria."""
366         # NOTE: We can't use _find_one because findParents takes a different
367         # set of arguments.
368         r = None
369         l = self.find_parents(name, attrs, 1)
370         if l:
371             r = l[0]
372         return r
373     findParent = find_parent  # BS3
374
375     def find_parents(self, name=None, attrs={}, limit=None, **kwargs):
376         """Returns the parents of this Tag that match the given
377         criteria."""
378
379         return self._find_all(name, attrs, None, limit, self.parents,
380                              **kwargs)
381     findParents = find_parents   # BS3
382     fetchParents = find_parents  # BS2
383
384     @property
385     def next(self):
386         return self.next_element
387
388     @property
389     def previous(self):
390         return self.previous_element
391
392     #These methods do the real heavy lifting.
393
394     def _find_one(self, method, name, attrs, text, **kwargs):
395         r = None
396         l = method(name, attrs, text, 1, **kwargs)
397         if l:
398             r = l[0]
399         return r
400
401     def _find_all(self, name, attrs, text, limit, generator, **kwargs):
402         "Iterates over a generator looking for things that match."
403
404         if isinstance(name, SoupStrainer):
405             strainer = name
406         elif text is None and not limit and not attrs and not kwargs:
407             # Optimization to find all tags.
408             if name is True or name is None:
409                 return [element for element in generator
410                         if isinstance(element, Tag)]
411             # Optimization to find all tags with a given name.
412             elif isinstance(name, basestring):
413                 return [element for element in generator
414                         if isinstance(element, Tag) and element.name == name]
415             else:
416                 strainer = SoupStrainer(name, attrs, text, **kwargs)
417         else:
418             # Build a SoupStrainer
419             strainer = SoupStrainer(name, attrs, text, **kwargs)
420         results = ResultSet(strainer)
421         while True:
422             try:
423                 i = next(generator)
424             except StopIteration:
425                 break
426             if i:
427                 found = strainer.search(i)
428                 if found:
429                     results.append(found)
430                     if limit and len(results) >= limit:
431                         break
432         return results
433
434     #These generators can be used to navigate starting from both
435     #NavigableStrings and Tags.
436     @property
437     def next_elements(self):
438         i = self.next_element
439         while i is not None:
440             yield i
441             i = i.next_element
442
443     @property
444     def next_siblings(self):
445         i = self.next_sibling
446         while i is not None:
447             yield i
448             i = i.next_sibling
449
450     @property
451     def previous_elements(self):
452         i = self.previous_element
453         while i is not None:
454             yield i
455             i = i.previous_element
456
457     @property
458     def previous_siblings(self):
459         i = self.previous_sibling
460         while i is not None:
461             yield i
462             i = i.previous_sibling
463
464     @property
465     def parents(self):
466         i = self.parent
467         while i is not None:
468             yield i
469             i = i.parent
470
471     # Methods for supporting CSS selectors.
472
473     tag_name_re = re.compile('^[a-z0-9]+$')
474
475     # /^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
476     #   \---/  \---/\-------------/    \-------/
477     #     |      |         |               |
478     #     |      |         |           The value
479     #     |      |    ~,|,^,$,* or =
480     #     |   Attribute
481     #    Tag
482     attribselect_re = re.compile(
483         r'^(?P<tag>\w+)?\[(?P<attribute>\w+)(?P<operator>[=~\|\^\$\*]?)' +
484         r'=?"?(?P<value>[^\]"]*)"?\]$'
485         )
486
487     def _attr_value_as_string(self, value, default=None):
488         """Force an attribute value into a string representation.
489
490         A multi-valued attribute will be converted into a
491         space-separated stirng.
492         """
493         value = self.get(value, default)
494         if isinstance(value, list) or isinstance(value, tuple):
495             value =" ".join(value)
496         return value
497
498     def _attribute_checker(self, operator, attribute, value=''):
499         """Create a function that performs a CSS selector operation.
500
501         Takes an operator, attribute and optional value. Returns a
502         function that will return True for elements that match that
503         combination.
504         """
505         if operator == '=':
506             # string representation of `attribute` is equal to `value`
507             return lambda el: el._attr_value_as_string(attribute) == value
508         elif operator == '~':
509             # space-separated list representation of `attribute`
510             # contains `value`
511             def _includes_value(element):
512                 attribute_value = element.get(attribute, [])
513                 if not isinstance(attribute_value, list):
514                     attribute_value = attribute_value.split()
515                 return value in attribute_value
516             return _includes_value
517         elif operator == '^':
518             # string representation of `attribute` starts with `value`
519             return lambda el: el._attr_value_as_string(
520                 attribute, '').startswith(value)
521         elif operator == '$':
522             # string represenation of `attribute` ends with `value`
523             return lambda el: el._attr_value_as_string(
524                 attribute, '').endswith(value)
525         elif operator == '*':
526             # string representation of `attribute` contains `value`
527             return lambda el: value in el._attr_value_as_string(attribute, '')
528         elif operator == '|':
529             # string representation of `attribute` is either exactly
530             # `value` or starts with `value` and then a dash.
531             def _is_or_starts_with_dash(element):
532                 attribute_value = element._attr_value_as_string(attribute, '')
533                 return (attribute_value == value or attribute_value.startswith(
534                         value + '-'))
535             return _is_or_starts_with_dash
536         else:
537             return lambda el: el.has_attr(attribute)
538
539     def select(self, selector):
540         """Perform a CSS selection operation on the current element."""
541         tokens = selector.split()
542         current_context = [self]
543         for index, token in enumerate(tokens):
544             if tokens[index - 1] == '>':
545                 # already found direct descendants in last step. skip this
546                 # step.
547                 continue
548             m = self.attribselect_re.match(token)
549             if m is not None:
550                 # Attribute selector
551                 tag, attribute, operator, value = m.groups()
552                 if not tag:
553                     tag = True
554                 checker = self._attribute_checker(operator, attribute, value)
555                 found = []
556                 for context in current_context:
557                     found.extend(
558                         [el for el in context.find_all(tag) if checker(el)])
559                 current_context = found
560                 continue
561
562             if '#' in token:
563                 # ID selector
564                 tag, id = token.split('#', 1)
565                 if tag == "":
566                     tag = True
567                 el = current_context[0].find(tag, {'id': id})
568                 if el is None:
569                     return [] # No match
570                 current_context = [el]
571                 continue
572
573             if '.' in token:
574                 # Class selector
575                 tag_name, klass = token.split('.', 1)
576                 if not tag_name:
577                     tag_name = True
578                 classes = set(klass.split('.'))
579                 found = []
580                 def classes_match(tag):
581                     if tag_name is not True and tag.name != tag_name:
582                         return False
583                     if not tag.has_attr('class'):
584                         return False
585                     return classes.issubset(tag['class'])
586                 for context in current_context:
587                     found.extend(context.find_all(classes_match))
588                 current_context = found
589                 continue
590
591             if token == '*':
592                 # Star selector
593                 found = []
594                 for context in current_context:
595                     found.extend(context.findAll(True))
596                 current_context = found
597                 continue
598
599             if token == '>':
600                 # Child selector
601                 tag = tokens[index + 1]
602                 if not tag:
603                     tag = True
604
605                 found = []
606                 for context in current_context:
607                     found.extend(context.find_all(tag, recursive=False))
608                 current_context = found
609                 continue
610
611             # Here we should just have a regular tag
612             if not self.tag_name_re.match(token):
613                 return []
614             found = []
615             for context in current_context:
616                 found.extend(context.findAll(token))
617             current_context = found
618         return current_context
619
620     # Old non-property versions of the generators, for backwards
621     # compatibility with BS3.
622     def nextGenerator(self):
623         return self.next_elements
624
625     def nextSiblingGenerator(self):
626         return self.next_siblings
627
628     def previousGenerator(self):
629         return self.previous_elements
630
631     def previousSiblingGenerator(self):
632         return self.previous_siblings
633
634     def parentGenerator(self):
635         return self.parents
636
637
638 class NavigableString(unicode, PageElement):
639
640     PREFIX = ''
641     SUFFIX = ''
642
643     def __new__(cls, value):
644         """Create a new NavigableString.
645
646         When unpickling a NavigableString, this method is called with
647         the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be
648         passed in to the superclass's __new__ or the superclass won't know
649         how to handle non-ASCII characters.
650         """
651         if isinstance(value, unicode):
652             return unicode.__new__(cls, value)
653         return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
654
655     def __getnewargs__(self):
656         return (unicode(self),)
657
658     def __getattr__(self, attr):
659         """text.string gives you text. This is for backwards
660         compatibility for Navigable*String, but for CData* it lets you
661         get the string without the CData wrapper."""
662         if attr == 'string':
663             return self
664         else:
665             raise AttributeError(
666                 "'%s' object has no attribute '%s'" % (
667                     self.__class__.__name__, attr))
668
669     def output_ready(self, formatter="minimal"):
670         output = self.format_string(self, formatter)
671         return self.PREFIX + output + self.SUFFIX
672
673
674 class PreformattedString(NavigableString):
675     """A NavigableString not subject to the normal formatting rules.
676
677     The string will be passed into the formatter (to trigger side effects),
678     but the return value will be ignored.
679     """
680
681     def output_ready(self, formatter="minimal"):
682         """CData strings are passed into the formatter.
683         But the return value is ignored."""
684         self.format_string(self, formatter)
685         return self.PREFIX + self + self.SUFFIX
686
687 class CData(PreformattedString):
688
689     PREFIX = u'<![CDATA['
690     SUFFIX = u']]>'
691
692 class ProcessingInstruction(PreformattedString):
693
694     PREFIX = u'<?'
695     SUFFIX = u'?>'
696
697 class Comment(PreformattedString):
698
699     PREFIX = u'<!--'
700     SUFFIX = u'-->'
701
702
703 class Declaration(PreformattedString):
704     PREFIX = u'<!'
705     SUFFIX = u'!>'
706
707
708 class Doctype(PreformattedString):
709
710     @classmethod
711     def for_name_and_ids(cls, name, pub_id, system_id):
712         value = name
713         if pub_id is not None:
714             value += ' PUBLIC "%s"' % pub_id
715             if system_id is not None:
716                 value += ' "%s"' % system_id
717         elif system_id is not None:
718             value += ' SYSTEM "%s"' % system_id
719
720         return Doctype(value)
721
722     PREFIX = u'<!DOCTYPE '
723     SUFFIX = u'>\n'
724
725
726 class Tag(PageElement):
727
728     """Represents a found HTML tag with its attributes and contents."""
729
730     def __init__(self, parser=None, builder=None, name=None, namespace=None,
731                  prefix=None, attrs=None, parent=None, previous=None):
732         "Basic constructor."
733
734         if parser is None:
735             self.parser_class = None
736         else:
737             # We don't actually store the parser object: that lets extracted
738             # chunks be garbage-collected.
739             self.parser_class = parser.__class__
740         if name is None:
741             raise ValueError("No value provided for new tag's name.")
742         self.name = name
743         self.namespace = namespace
744         self.prefix = prefix
745         if attrs is None:
746             attrs = {}
747         elif builder.cdata_list_attributes:
748             attrs = builder._replace_cdata_list_attribute_values(
749                 self.name, attrs)
750         else:
751             attrs = dict(attrs)
752         self.attrs = attrs
753         self.contents = []
754         self.setup(parent, previous)
755         self.hidden = False
756
757         # Set up any substitutions, such as the charset in a META tag.
758         if builder is not None:
759             builder.set_up_substitutions(self)
760             self.can_be_empty_element = builder.can_be_empty_element(name)
761         else:
762             self.can_be_empty_element = False
763
764     parserClass = _alias("parser_class")  # BS3
765
766     @property
767     def is_empty_element(self):
768         """Is this tag an empty-element tag? (aka a self-closing tag)
769
770         A tag that has contents is never an empty-element tag.
771
772         A tag that has no contents may or may not be an empty-element
773         tag. It depends on the builder used to create the tag. If the
774         builder has a designated list of empty-element tags, then only
775         a tag whose name shows up in that list is considered an
776         empty-element tag.
777
778         If the builder has no designated list of empty-element tags,
779         then any tag with no contents is an empty-element tag.
780         """
781         return len(self.contents) == 0 and self.can_be_empty_element
782     isSelfClosing = is_empty_element  # BS3
783
784     @property
785     def string(self):
786         """Convenience property to get the single string within this tag.
787
788         :Return: If this tag has a single string child, return value
789          is that string. If this tag has no children, or more than one
790          child, return value is None. If this tag has one child tag,
791          return value is the 'string' attribute of the child tag,
792          recursively.
793         """
794         if len(self.contents) != 1:
795             return None
796         child = self.contents[0]
797         if isinstance(child, NavigableString):
798             return child
799         return child.string
800
801     @string.setter
802     def string(self, string):
803         self.clear()
804         self.append(string.__class__(string))
805
806     def _all_strings(self, strip=False):
807         """Yield all child strings, possibly stripping them."""
808         for descendant in self.descendants:
809             if not isinstance(descendant, NavigableString):
810                 continue
811             if strip:
812                 descendant = descendant.strip()
813                 if len(descendant) == 0:
814                     continue
815             yield descendant
816     strings = property(_all_strings)
817
818     @property
819     def stripped_strings(self):
820         for string in self._all_strings(True):
821             yield string
822
823     def get_text(self, separator=u"", strip=False):
824         """
825         Get all child strings, concatenated using the given separator.
826         """
827         return separator.join([s for s in self._all_strings(strip)])
828     getText = get_text
829     text = property(get_text)
830
831     def decompose(self):
832         """Recursively destroys the contents of this tree."""
833         self.extract()
834         i = self
835         while i is not None:
836             next = i.next_element
837             i.__dict__.clear()
838             i = next
839
840     def clear(self, decompose=False):
841         """
842         Extract all children. If decompose is True, decompose instead.
843         """
844         if decompose:
845             for element in self.contents[:]:
846                 if isinstance(element, Tag):
847                     element.decompose()
848                 else:
849                     element.extract()
850         else:
851             for element in self.contents[:]:
852                 element.extract()
853
854     def index(self, element):
855         """
856         Find the index of a child by identity, not value. Avoids issues with
857         tag.contents.index(element) getting the index of equal elements.
858         """
859         for i, child in enumerate(self.contents):
860             if child is element:
861                 return i
862         raise ValueError("Tag.index: element not in tag")
863
864     def get(self, key, default=None):
865         """Returns the value of the 'key' attribute for the tag, or
866         the value given for 'default' if it doesn't have that
867         attribute."""
868         return self.attrs.get(key, default)
869
870     def has_attr(self, key):
871         return key in self.attrs
872
873     def __hash__(self):
874         return str(self).__hash__()
875
876     def __getitem__(self, key):
877         """tag[key] returns the value of the 'key' attribute for the tag,
878         and throws an exception if it's not there."""
879         return self.attrs[key]
880
881     def __iter__(self):
882         "Iterating over a tag iterates over its contents."
883         return iter(self.contents)
884
885     def __len__(self):
886         "The length of a tag is the length of its list of contents."
887         return len(self.contents)
888
889     def __contains__(self, x):
890         return x in self.contents
891
892     def __nonzero__(self):
893         "A tag is non-None even if it has no contents."
894         return True
895
896     def __setitem__(self, key, value):
897         """Setting tag[key] sets the value of the 'key' attribute for the
898         tag."""
899         self.attrs[key] = value
900
901     def __delitem__(self, key):
902         "Deleting tag[key] deletes all 'key' attributes for the tag."
903         self.attrs.pop(key, None)
904
905     def __call__(self, *args, **kwargs):
906         """Calling a tag like a function is the same as calling its
907         find_all() method. Eg. tag('a') returns a list of all the A tags
908         found within this tag."""
909         return self.find_all(*args, **kwargs)
910
911     def __getattr__(self, tag):
912         #print "Getattr %s.%s" % (self.__class__, tag)
913         if len(tag) > 3 and tag.endswith('Tag'):
914             # BS3: soup.aTag -> "soup.find("a")
915             tag_name = tag[:-3]
916             warnings.warn(
917                 '.%sTag is deprecated, use .find("%s") instead.' % (
918                     tag_name, tag_name))
919             return self.find(tag_name)
920         # We special case contents to avoid recursion.
921         elif not tag.startswith("__") and not tag=="contents":
922             return self.find(tag)
923         raise AttributeError(
924             "'%s' object has no attribute '%s'" % (self.__class__, tag))
925
926     def __eq__(self, other):
927         """Returns true iff this tag has the same name, the same attributes,
928         and the same contents (recursively) as the given tag."""
929         if self is other:
930             return True
931         if (not hasattr(other, 'name') or
932             not hasattr(other, 'attrs') or
933             not hasattr(other, 'contents') or
934             self.name != other.name or
935             self.attrs != other.attrs or
936             len(self) != len(other)):
937             return False
938         for i, my_child in enumerate(self.contents):
939             if my_child != other.contents[i]:
940                 return False
941         return True
942
943     def __ne__(self, other):
944         """Returns true iff this tag is not identical to the other tag,
945         as defined in __eq__."""
946         return not self == other
947
948     def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING):
949         """Renders this tag as a string."""
950         return self.encode(encoding)
951
952     def __unicode__(self):
953         return self.decode()
954
955     def __str__(self):
956         return self.encode()
957
958     if PY3K:
959         __str__ = __repr__ = __unicode__
960
961     def encode(self, encoding=DEFAULT_OUTPUT_ENCODING,
962                indent_level=None, formatter="minimal",
963                errors="xmlcharrefreplace"):
964         # Turn the data structure into Unicode, then encode the
965         # Unicode.
966         u = self.decode(indent_level, encoding, formatter)
967         return u.encode(encoding, errors)
968
969     def decode(self, indent_level=None,
970                eventual_encoding=DEFAULT_OUTPUT_ENCODING,
971                formatter="minimal"):
972         """Returns a Unicode representation of this tag and its contents.
973
974         :param eventual_encoding: The tag is destined to be
975            encoded into this encoding. This method is _not_
976            responsible for performing that encoding. This information
977            is passed in so that it can be substituted in if the
978            document contains a <META> tag that mentions the document's
979            encoding.
980         """
981         attrs = []
982         if self.attrs:
983             for key, val in sorted(self.attrs.items()):
984                 if val is None:
985                     decoded = key
986                 else:
987                     if isinstance(val, list) or isinstance(val, tuple):
988                         val = ' '.join(val)
989                     elif not isinstance(val, basestring):
990                         val = unicode(val)
991                     elif (
992                         isinstance(val, AttributeValueWithCharsetSubstitution)
993                         and eventual_encoding is not None):
994                         val = val.encode(eventual_encoding)
995
996                     text = self.format_string(val, formatter)
997                     decoded = (
998                         unicode(key) + '='
999                         + EntitySubstitution.quoted_attribute_value(text))
1000                 attrs.append(decoded)
1001         close = ''
1002         closeTag = ''
1003
1004         prefix = ''
1005         if self.prefix:
1006             prefix = self.prefix + ":"
1007
1008         if self.is_empty_element:
1009             close = '/'
1010         else:
1011             closeTag = '</%s%s>' % (prefix, self.name)
1012
1013         pretty_print = (indent_level is not None)
1014         if pretty_print:
1015             space = (' ' * (indent_level - 1))
1016             indent_contents = indent_level + 1
1017         else:
1018             space = ''
1019             indent_contents = None
1020         contents = self.decode_contents(
1021             indent_contents, eventual_encoding, formatter)
1022
1023         if self.hidden:
1024             # This is the 'document root' object.
1025             s = contents
1026         else:
1027             s = []
1028             attribute_string = ''
1029             if attrs:
1030                 attribute_string = ' ' + ' '.join(attrs)
1031             if pretty_print:
1032                 s.append(space)
1033             s.append('<%s%s%s%s>' % (
1034                     prefix, self.name, attribute_string, close))
1035             if pretty_print:
1036                 s.append("\n")
1037             s.append(contents)
1038             if pretty_print and contents and contents[-1] != "\n":
1039                 s.append("\n")
1040             if pretty_print and closeTag:
1041                 s.append(space)
1042             s.append(closeTag)
1043             if pretty_print and closeTag and self.next_sibling:
1044                 s.append("\n")
1045             s = ''.join(s)
1046         return s
1047
1048     def prettify(self, encoding=None, formatter="minimal"):
1049         if encoding is None:
1050             return self.decode(True, formatter=formatter)
1051         else:
1052             return self.encode(encoding, True, formatter=formatter)
1053
1054     def decode_contents(self, indent_level=None,
1055                        eventual_encoding=DEFAULT_OUTPUT_ENCODING,
1056                        formatter="minimal"):
1057         """Renders the contents of this tag as a Unicode string.
1058
1059         :param eventual_encoding: The tag is destined to be
1060            encoded into this encoding. This method is _not_
1061            responsible for performing that encoding. This information
1062            is passed in so that it can be substituted in if the
1063            document contains a <META> tag that mentions the document's
1064            encoding.
1065         """
1066         pretty_print = (indent_level is not None)
1067         s = []
1068         for c in self:
1069             text = None
1070             if isinstance(c, NavigableString):
1071                 text = c.output_ready(formatter)
1072             elif isinstance(c, Tag):
1073                 s.append(c.decode(indent_level, eventual_encoding,
1074                                   formatter))
1075             if text and indent_level:
1076                 text = text.strip()
1077             if text:
1078                 if pretty_print:
1079                     s.append(" " * (indent_level - 1))
1080                 s.append(text)
1081                 if pretty_print:
1082                     s.append("\n")
1083         return ''.join(s)
1084
1085     def encode_contents(
1086         self, indent_level=None, encoding=DEFAULT_OUTPUT_ENCODING,
1087         formatter="minimal"):
1088         """Renders the contents of this tag as a bytestring."""
1089         contents = self.decode_contents(indent_level, encoding, formatter)
1090         return contents.encode(encoding)
1091
1092     # Old method for BS3 compatibility
1093     def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING,
1094                        prettyPrint=False, indentLevel=0):
1095         if not prettyPrint:
1096             indentLevel = None
1097         return self.encode_contents(
1098             indent_level=indentLevel, encoding=encoding)
1099
1100     #Soup methods
1101
1102     def find(self, name=None, attrs={}, recursive=True, text=None,
1103              **kwargs):
1104         """Return only the first child of this Tag matching the given
1105         criteria."""
1106         r = None
1107         l = self.find_all(name, attrs, recursive, text, 1, **kwargs)
1108         if l:
1109             r = l[0]
1110         return r
1111     findChild = find
1112
1113     def find_all(self, name=None, attrs={}, recursive=True, text=None,
1114                  limit=None, **kwargs):
1115         """Extracts a list of Tag objects that match the given
1116         criteria.  You can specify the name of the Tag and any
1117         attributes you want the Tag to have.
1118
1119         The value of a key-value pair in the 'attrs' map can be a
1120         string, a list of strings, a regular expression object, or a
1121         callable that takes a string and returns whether or not the
1122         string matches for some custom definition of 'matches'. The
1123         same is true of the tag name."""
1124
1125         generator = self.descendants
1126         if not recursive:
1127             generator = self.children
1128         return self._find_all(name, attrs, text, limit, generator, **kwargs)
1129     findAll = find_all       # BS3
1130     findChildren = find_all  # BS2
1131
1132     #Generator methods
1133     @property
1134     def children(self):
1135         # return iter() to make the purpose of the method clear
1136         return iter(self.contents)  # XXX This seems to be untested.
1137
1138     @property
1139     def descendants(self):
1140         if not len(self.contents):
1141             return
1142         stopNode = self._last_descendant().next_element
1143         current = self.contents[0]
1144         while current is not stopNode:
1145             yield current
1146             current = current.next_element
1147
1148     # Old names for backwards compatibility
1149     def childGenerator(self):
1150         return self.children
1151
1152     def recursiveChildGenerator(self):
1153         return self.descendants
1154
1155     # This was kind of misleading because has_key() (attributes) was
1156     # different from __in__ (contents). has_key() is gone in Python 3,
1157     # anyway.
1158     has_key = has_attr
1159
1160 # Next, a couple classes to represent queries and their results.
1161 class SoupStrainer(object):
1162     """Encapsulates a number of ways of matching a markup element (tag or
1163     text)."""
1164
1165     def __init__(self, name=None, attrs={}, text=None, **kwargs):
1166         self.name = self._normalize_search_value(name)
1167         if not isinstance(attrs, dict):
1168             # Treat a non-dict value for attrs as a search for the 'class'
1169             # attribute.
1170             kwargs['class'] = attrs
1171             attrs = None
1172
1173         if 'class_' in kwargs:
1174             # Treat class_="foo" as a search for the 'class'
1175             # attribute, overriding any non-dict value for attrs.
1176             kwargs['class'] = kwargs['class_']
1177             del kwargs['class_']
1178
1179         if kwargs:
1180             if attrs:
1181                 attrs = attrs.copy()
1182                 attrs.update(kwargs)
1183             else:
1184                 attrs = kwargs
1185         normalized_attrs = {}
1186         for key, value in attrs.items():
1187             normalized_attrs[key] = self._normalize_search_value(value)
1188
1189         self.attrs = normalized_attrs
1190         self.text = self._normalize_search_value(text)
1191
1192     def _normalize_search_value(self, value):
1193         # Leave it alone if it's a Unicode string, a callable, a
1194         # regular expression, a boolean, or None.
1195         if (isinstance(value, unicode) or callable(value) or hasattr(value, 'match')
1196             or isinstance(value, bool) or value is None):
1197             return value
1198
1199         # If it's a bytestring, convert it to Unicode, treating it as UTF-8.
1200         if isinstance(value, bytes):
1201             return value.decode("utf8")
1202
1203         # If it's listlike, convert it into a list of strings.
1204         if hasattr(value, '__iter__'):
1205             new_value = []
1206             for v in value:
1207                 if (hasattr(v, '__iter__') and not isinstance(v, bytes)
1208                     and not isinstance(v, unicode)):
1209                     # This is almost certainly the user's mistake. In the
1210                     # interests of avoiding infinite loops, we'll let
1211                     # it through as-is rather than doing a recursive call.
1212                     new_value.append(v)
1213                 else:
1214                     new_value.append(self._normalize_search_value(v))
1215             return new_value
1216
1217         # Otherwise, convert it into a Unicode string.
1218         # The unicode(str()) thing is so this will do the same thing on Python 2
1219         # and Python 3.
1220         return unicode(str(value))
1221
1222     def __str__(self):
1223         if self.text:
1224             return self.text
1225         else:
1226             return "%s|%s" % (self.name, self.attrs)
1227
1228     def search_tag(self, markup_name=None, markup_attrs={}):
1229         found = None
1230         markup = None
1231         if isinstance(markup_name, Tag):
1232             markup = markup_name
1233             markup_attrs = markup
1234         call_function_with_tag_data = (
1235             isinstance(self.name, collections.Callable)
1236             and not isinstance(markup_name, Tag))
1237
1238         if ((not self.name)
1239             or call_function_with_tag_data
1240             or (markup and self._matches(markup, self.name))
1241             or (not markup and self._matches(markup_name, self.name))):
1242             if call_function_with_tag_data:
1243                 match = self.name(markup_name, markup_attrs)
1244             else:
1245                 match = True
1246                 markup_attr_map = None
1247                 for attr, match_against in list(self.attrs.items()):
1248                     if not markup_attr_map:
1249                         if hasattr(markup_attrs, 'get'):
1250                             markup_attr_map = markup_attrs
1251                         else:
1252                             markup_attr_map = {}
1253                             for k, v in markup_attrs:
1254                                 markup_attr_map[k] = v
1255                     attr_value = markup_attr_map.get(attr)
1256                     if not self._matches(attr_value, match_against):
1257                         match = False
1258                         break
1259             if match:
1260                 if markup:
1261                     found = markup
1262                 else:
1263                     found = markup_name
1264         if found and self.text and not self._matches(found.string, self.text):
1265             found = None
1266         return found
1267     searchTag = search_tag
1268
1269     def search(self, markup):
1270         # print 'looking for %s in %s' % (self, markup)
1271         found = None
1272         # If given a list of items, scan it for a text element that
1273         # matches.
1274         if hasattr(markup, '__iter__') and not isinstance(markup, (Tag, basestring)):
1275             for element in markup:
1276                 if isinstance(element, NavigableString) \
1277                        and self.search(element):
1278                     found = element
1279                     break
1280         # If it's a Tag, make sure its name or attributes match.
1281         # Don't bother with Tags if we're searching for text.
1282         elif isinstance(markup, Tag):
1283             if not self.text or self.name or self.attrs:
1284                 found = self.search_tag(markup)
1285         # If it's text, make sure the text matches.
1286         elif isinstance(markup, NavigableString) or \
1287                  isinstance(markup, basestring):
1288             if not self.name and not self.attrs and self._matches(markup, self.text):
1289                 found = markup
1290         else:
1291             raise Exception(
1292                 "I don't know how to match against a %s" % markup.__class__)
1293         return found
1294
1295     def _matches(self, markup, match_against):
1296         # print u"Matching %s against %s" % (markup, match_against)
1297         result = False
1298         if isinstance(markup, list) or isinstance(markup, tuple):
1299             # This should only happen when searching a multi-valued attribute
1300             # like 'class'.
1301             if (isinstance(match_against, unicode)
1302                 and ' ' in match_against):
1303                 # A bit of a special case. If they try to match "foo
1304                 # bar" on a multivalue attribute's value, only accept
1305                 # the literal value "foo bar"
1306                 #
1307                 # XXX This is going to be pretty slow because we keep
1308                 # splitting match_against. But it shouldn't come up
1309                 # too often.
1310                 return (whitespace_re.split(match_against) == markup)
1311             else:
1312                 for item in markup:
1313                     if self._matches(item, match_against):
1314                         return True
1315                 return False
1316
1317         if match_against is True:
1318             # True matches any non-None value.
1319             return markup is not None
1320
1321         if isinstance(match_against, collections.Callable):
1322             return match_against(markup)
1323
1324         # Custom callables take the tag as an argument, but all
1325         # other ways of matching match the tag name as a string.
1326         if isinstance(markup, Tag):
1327             markup = markup.name
1328
1329         # Ensure that `markup` is either a Unicode string, or None.
1330         markup = self._normalize_search_value(markup)
1331
1332         if markup is None:
1333             # None matches None, False, an empty string, an empty list, and so on.
1334             return not match_against
1335
1336         if isinstance(match_against, unicode):
1337             # Exact string match
1338             return markup == match_against
1339
1340         if hasattr(match_against, 'match'):
1341             # Regexp match
1342             return match_against.search(markup)
1343
1344         if hasattr(match_against, '__iter__'):
1345             # The markup must be an exact match against something
1346             # in the iterable.
1347             return markup in match_against
1348
1349
1350 class ResultSet(list):
1351     """A ResultSet is just a list that keeps track of the SoupStrainer
1352     that created it."""
1353     def __init__(self, source):
1354         list.__init__([])
1355         self.source = source