From 65b430b23a2737b63d1c211032ec8bc89953731f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 17 Apr 2007 07:16:25 +0000 Subject: [PATCH] py2exe functionality added for standalone windows executables. Initial implementation of HTML->LRF conversion --- LICENSE | 282 ++++ icons/library.ico | Bin 0 -> 83558 bytes installer.nsi | 24 + setup.py | 19 +- src/libprs500/cli/main.py | 2 +- src/libprs500/gui/main.py | 4 +- src/libprs500/lrf/__init__.py | 3 + src/libprs500/lrf/html/BeautifulSoup.py | 1767 +++++++++++++++++++++++ src/libprs500/lrf/html/__init__.py | 20 + src/libprs500/lrf/html/convert.py | 334 +++++ src/libprs500/lrf/lrfcreator.py | 68 - src/libprs500/lrf/lrftypes.py | 93 -- src/libprs500/lrf/makelrf.py | 85 ++ src/libprs500/lrf/pylrs/pylrs.py | 1 - 14 files changed, 2536 insertions(+), 166 deletions(-) create mode 100644 LICENSE create mode 100644 icons/library.ico create mode 100644 installer.nsi create mode 100644 src/libprs500/lrf/html/BeautifulSoup.py create mode 100644 src/libprs500/lrf/html/__init__.py create mode 100644 src/libprs500/lrf/html/convert.py delete mode 100644 src/libprs500/lrf/lrfcreator.py delete mode 100644 src/libprs500/lrf/lrftypes.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..fbaccb90d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,282 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + diff --git a/icons/library.ico b/icons/library.ico new file mode 100644 index 0000000000000000000000000000000000000000..b4aba2d7941db006bca48e0adb6a2e19d2e15cfe GIT binary patch literal 83558 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 len(self.contents): + newChild.nextSibling = None + + parent = self + parentsNextSibling = None + while not parentsNextSibling: + parentsNextSibling = parent.nextSibling + parent = parent.parent + if not parent: # This is the last element in the document. + break + if parentsNextSibling: + newChildsLastElement.next = parentsNextSibling + else: + newChildsLastElement.next = None + else: + nextChild = self.contents[position] + newChild.nextSibling = nextChild + if newChild.nextSibling: + newChild.nextSibling.previousSibling = newChild + newChildsLastElement.next = nextChild + + if newChildsLastElement.next: + newChildsLastElement.next.previous = newChildsLastElement + self.contents.insert(position, newChild) + + def findNext(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears after this Tag in the document.""" + return self._findOne(self.findAllNext, name, attrs, text, **kwargs) + + def findAllNext(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + before after Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.nextGenerator) + + def findNextSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears after this Tag in the document.""" + return self._findOne(self.findNextSiblings, name, attrs, text, + **kwargs) + + def findNextSiblings(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.nextSiblingGenerator, **kwargs) + fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x + + def findPrevious(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears before this Tag in the document.""" + return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs) + + def findAllPrevious(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.previousGenerator, + **kwargs) + fetchPrevious = findAllPrevious # Compatibility with pre-3.x + + def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears before this Tag in the document.""" + return self._findOne(self.findPreviousSiblings, name, attrs, text, + **kwargs) + + def findPreviousSiblings(self, name=None, attrs={}, text=None, + limit=None, **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.previousSiblingGenerator, **kwargs) + fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x + + def findParent(self, name=None, attrs={}, **kwargs): + """Returns the closest parent of this Tag that matches the given + criteria.""" + # NOTE: We can't use _findOne because findParents takes a different + # set of arguments. + r = None + l = self.findParents(name, attrs, 1) + if l: + r = l[0] + return r + + def findParents(self, name=None, attrs={}, limit=None, **kwargs): + """Returns the parents of this Tag that match the given + criteria.""" + + return self._findAll(name, attrs, None, limit, self.parentGenerator, + **kwargs) + fetchParents = findParents # Compatibility with pre-3.x + + #These methods do the real heavy lifting. + + def _findOne(self, method, name, attrs, text, **kwargs): + r = None + l = method(name, attrs, text, 1, **kwargs) + if l: + r = l[0] + return r + + def _findAll(self, name, attrs, text, limit, generator, **kwargs): + "Iterates over a generator looking for things that match." + + if isinstance(name, SoupStrainer): + strainer = name + else: + # Build a SoupStrainer + strainer = SoupStrainer(name, attrs, text, **kwargs) + results = ResultSet(strainer) + g = generator() + while True: + try: + i = g.next() + except StopIteration: + break + if i: + found = strainer.search(i) + if found: + results.append(found) + if limit and len(results) >= limit: + break + return results + + #These Generators can be used to navigate starting from both + #NavigableStrings and Tags. + def nextGenerator(self): + i = self + while i: + i = i.next + yield i + + def nextSiblingGenerator(self): + i = self + while i: + i = i.nextSibling + yield i + + def previousGenerator(self): + i = self + while i: + i = i.previous + yield i + + def previousSiblingGenerator(self): + i = self + while i: + i = i.previousSibling + yield i + + def parentGenerator(self): + i = self + while i: + i = i.parent + yield i + + # Utility methods + def substituteEncoding(self, str, encoding=None): + encoding = encoding or "utf-8" + return str.replace("%SOUP-ENCODING%", encoding) + + def toEncoding(self, s, encoding=None): + """Encodes an object to a string in some encoding, or to Unicode. + .""" + if isinstance(s, unicode): + if encoding: + s = s.encode(encoding) + elif isinstance(s, str): + if encoding: + s = s.encode(encoding) + else: + s = unicode(s) + else: + if encoding: + s = self.toEncoding(str(s), encoding) + else: + s = unicode(s) + return s + +class NavigableString(unicode, PageElement): + + def __getattr__(self, attr): + """text.string gives you text. This is for backwards + compatibility for Navigable*String, but for CData* it lets you + get the string without the CData wrapper.""" + if attr == 'string': + return self + else: + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) + + def __unicode__(self): + return self.__str__(None) + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + if encoding: + return self.encode(encoding) + else: + return self + +class CData(NavigableString): + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class ProcessingInstruction(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + output = self + if "%SOUP-ENCODING%" in output: + output = self.substituteEncoding(output, encoding) + return "" % self.toEncoding(output, encoding) + +class Comment(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class Declaration(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class Tag(PageElement): + + """Represents a found HTML tag with its attributes and contents.""" + + XML_SPECIAL_CHARS_TO_ENTITIES = { "'" : "squot", + '"' : "quote", + "&" : "amp", + "<" : "lt", + ">" : "gt" } + + def __init__(self, parser, name, attrs=None, parent=None, + previous=None): + "Basic constructor." + + # We don't actually store the parser object: that lets extracted + # chunks be garbage-collected + self.parserClass = parser.__class__ + self.isSelfClosing = parser.isSelfClosingTag(name) + self.name = name + if attrs == None: + attrs = [] + self.attrs = attrs + self.contents = [] + self.setup(parent, previous) + self.hidden = False + self.containsSubstitutions = False + + def get(self, key, default=None): + """Returns the value of the 'key' attribute for the tag, or + the value given for 'default' if it doesn't have that + attribute.""" + return self._getAttrMap().get(key, default) + + def has_key(self, key): + return self._getAttrMap().has_key(key) + + def __getitem__(self, key): + """tag[key] returns the value of the 'key' attribute for the tag, + and throws an exception if it's not there.""" + return self._getAttrMap()[key] + + def __iter__(self): + "Iterating over a tag iterates over its contents." + return iter(self.contents) + + def __len__(self): + "The length of a tag is the length of its list of contents." + return len(self.contents) + + def __contains__(self, x): + return x in self.contents + + def __nonzero__(self): + "A tag is non-None even if it has no contents." + return True + + def __setitem__(self, key, value): + """Setting tag[key] sets the value of the 'key' attribute for the + tag.""" + self._getAttrMap() + self.attrMap[key] = value + found = False + for i in range(0, len(self.attrs)): + if self.attrs[i][0] == key: + self.attrs[i] = (key, value) + found = True + if not found: + self.attrs.append((key, value)) + self._getAttrMap()[key] = value + + def __delitem__(self, key): + "Deleting tag[key] deletes all 'key' attributes for the tag." + for item in self.attrs: + if item[0] == key: + self.attrs.remove(item) + #We don't break because bad HTML can define the same + #attribute multiple times. + self._getAttrMap() + if self.attrMap.has_key(key): + del self.attrMap[key] + + def __call__(self, *args, **kwargs): + """Calling a tag like a function is the same as calling its + findAll() method. Eg. tag('a') returns a list of all the A tags + found within this tag.""" + return apply(self.findAll, args, kwargs) + + def __getattr__(self, tag): + #print "Getattr %s.%s" % (self.__class__, tag) + if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: + return self.find(tag[:-3]) + elif tag.find('__') != 0: + return self.find(tag) + + def __eq__(self, other): + """Returns true iff this tag has the same name, the same attributes, + and the same contents (recursively) as the given tag. + + NOTE: right now this will return false if two tags have the + same attributes in a different order. Should this be fixed?""" + if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other): + return False + for i in range(0, len(self.contents)): + if self.contents[i] != other.contents[i]: + return False + return True + + def __ne__(self, other): + """Returns true iff this tag is not identical to the other tag, + as defined in __eq__.""" + return not self == other + + def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING): + """Renders this tag as a string.""" + return self.__str__(encoding) + + def __unicode__(self): + return self.__str__(None) + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Returns a string or Unicode representation of this tag and + its contents. To get Unicode, pass None for encoding. + + NOTE: since Python's HTML parser consumes whitespace, this + method is not certain to reproduce the whitespace present in + the original string.""" + + encodedName = self.toEncoding(self.name, encoding) + + attrs = [] + if self.attrs: + for key, val in self.attrs: + fmt = '%s="%s"' + if isString(val): + if self.containsSubstitutions and '%SOUP-ENCODING%' in val: + val = self.substituteEncoding(val, encoding) + + # The attribute value either: + # + # * Contains no embedded double quotes or single quotes. + # No problem: we enclose it in double quotes. + # * Contains embedded single quotes. No problem: + # double quotes work here too. + # * Contains embedded double quotes. No problem: + # we enclose it in single quotes. + # * Embeds both single _and_ double quotes. This + # can't happen naturally, but it can happen if + # you modify an attribute value after parsing + # the document. Now we have a bit of a + # problem. We solve it by enclosing the + # attribute in single quotes, and escaping any + # embedded single quotes to XML entities. + if '"' in val: + fmt = "%s='%s'" + # This can't happen naturally, but it can happen + # if you modify an attribute value after parsing. + if "'" in val: + val = val.replace("'", "&squot;") + + # Now we're okay w/r/t quotes. But the attribute + # value might also contain angle brackets, or + # ampersands that aren't part of entities. We need + # to escape those to XML entities too. + val = re.sub("([<>]|&(?![^\s]+;))", + lambda x: "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";", + val) + + attrs.append(fmt % (self.toEncoding(key, encoding), + self.toEncoding(val, encoding))) + close = '' + closeTag = '' + if self.isSelfClosing: + close = ' /' + else: + closeTag = '' % encodedName + + indentTag, indentContents = 0, 0 + if prettyPrint: + indentTag = indentLevel + space = (' ' * (indentTag-1)) + indentContents = indentTag + 1 + contents = self.renderContents(encoding, prettyPrint, indentContents) + if self.hidden: + s = contents + else: + s = [] + attributeString = '' + if attrs: + attributeString = ' ' + ' '.join(attrs) + if prettyPrint: + s.append(space) + s.append('<%s%s%s>' % (encodedName, attributeString, close)) + if prettyPrint: + s.append("\n") + s.append(contents) + if prettyPrint and contents and contents[-1] != "\n": + s.append("\n") + if prettyPrint and closeTag: + s.append(space) + s.append(closeTag) + if prettyPrint and closeTag and self.nextSibling: + s.append("\n") + s = ''.join(s) + return s + + def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): + return self.__str__(encoding, True) + + def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Renders the contents of this tag as a string in the given + encoding. If encoding is None, returns a Unicode string..""" + s=[] + for c in self: + text = None + if isinstance(c, NavigableString): + text = c.__str__(encoding) + elif isinstance(c, Tag): + s.append(c.__str__(encoding, prettyPrint, indentLevel)) + if text and prettyPrint: + text = text.strip() + if text: + if prettyPrint: + s.append(" " * (indentLevel-1)) + s.append(text) + if prettyPrint: + s.append("\n") + return ''.join(s) + + #Soup methods + + def find(self, name=None, attrs={}, recursive=True, text=None, + **kwargs): + """Return only the first child of this Tag matching the given + criteria.""" + r = None + l = self.findAll(name, attrs, recursive, text, 1, **kwargs) + if l: + r = l[0] + return r + findChild = find + + def findAll(self, name=None, attrs={}, recursive=True, text=None, + limit=None, **kwargs): + """Extracts a list of Tag objects that match the given + criteria. You can specify the name of the Tag and any + attributes you want the Tag to have. + + The value of a key-value pair in the 'attrs' map can be a + string, a list of strings, a regular expression object, or a + callable that takes a string and returns whether or not the + string matches for some custom definition of 'matches'. The + same is true of the tag name.""" + generator = self.recursiveChildGenerator + if not recursive: + generator = self.childGenerator + return self._findAll(name, attrs, text, limit, generator, **kwargs) + findChildren = findAll + + # Pre-3.x compatibility methods + first = find + fetch = findAll + + def fetchText(self, text=None, recursive=True, limit=None): + return self.findAll(text=text, recursive=recursive, limit=limit) + + def firstText(self, text=None, recursive=True): + return self.find(text=text, recursive=recursive) + + #Utility methods + + def append(self, tag): + """Appends the given tag to the contents of this tag.""" + self.contents.append(tag) + + #Private methods + + def _getAttrMap(self): + """Initializes a map representation of this tag's attributes, + if not already initialized.""" + if not getattr(self, 'attrMap'): + self.attrMap = {} + for (key, value) in self.attrs: + self.attrMap[key] = value + return self.attrMap + + #Generator methods + def childGenerator(self): + for i in range(0, len(self.contents)): + yield self.contents[i] + raise StopIteration + + def recursiveChildGenerator(self): + stack = [(self, 0)] + while stack: + tag, start = stack.pop() + if isinstance(tag, Tag): + for i in range(start, len(tag.contents)): + a = tag.contents[i] + yield a + if isinstance(a, Tag) and tag.contents: + if i < len(tag.contents) - 1: + stack.append((tag, i+1)) + stack.append((a, 0)) + break + raise StopIteration + +# Next, a couple classes to represent queries and their results. +class SoupStrainer: + """Encapsulates a number of ways of matching a markup element (tag or + text).""" + + def __init__(self, name=None, attrs={}, text=None, **kwargs): + self.name = name + if isString(attrs): + kwargs['class'] = attrs + attrs = None + if kwargs: + if attrs: + attrs = attrs.copy() + attrs.update(kwargs) + else: + attrs = kwargs + self.attrs = attrs + self.text = text + + def __str__(self): + if self.text: + return self.text + else: + return "%s|%s" % (self.name, self.attrs) + + def searchTag(self, markupName=None, markupAttrs={}): + found = None + markup = None + if isinstance(markupName, Tag): + markup = markupName + markupAttrs = markup + callFunctionWithTagData = callable(self.name) \ + and not isinstance(markupName, Tag) + + if (not self.name) \ + or callFunctionWithTagData \ + or (markup and self._matches(markup, self.name)) \ + or (not markup and self._matches(markupName, self.name)): + if callFunctionWithTagData: + match = self.name(markupName, markupAttrs) + else: + match = True + markupAttrMap = None + for attr, matchAgainst in self.attrs.items(): + if not markupAttrMap: + if hasattr(markupAttrs, 'get'): + markupAttrMap = markupAttrs + else: + markupAttrMap = {} + for k,v in markupAttrs: + markupAttrMap[k] = v + attrValue = markupAttrMap.get(attr) + if not self._matches(attrValue, matchAgainst): + match = False + break + if match: + if markup: + found = markup + else: + found = markupName + return found + + def search(self, markup): + #print 'looking for %s in %s' % (self, markup) + found = None + # If given a list of items, scan it for a text element that + # matches. + if isList(markup) and not isinstance(markup, Tag): + for element in markup: + if isinstance(element, NavigableString) \ + and self.search(element): + found = element + break + # If it's a Tag, make sure its name or attributes match. + # Don't bother with Tags if we're searching for text. + elif isinstance(markup, Tag): + if not self.text: + found = self.searchTag(markup) + # If it's text, make sure the text matches. + elif isinstance(markup, NavigableString) or \ + isString(markup): + if self._matches(markup, self.text): + found = markup + else: + raise Exception, "I don't know how to match against a %s" \ + % markup.__class__ + return found + + def _matches(self, markup, matchAgainst): + #print "Matching %s against %s" % (markup, matchAgainst) + result = False + if matchAgainst == True and type(matchAgainst) == types.BooleanType: + result = markup != None + elif callable(matchAgainst): + result = matchAgainst(markup) + else: + #Custom match methods take the tag as an argument, but all + #other ways of matching match the tag name as a string. + if isinstance(markup, Tag): + markup = markup.name + if markup and not isString(markup): + markup = unicode(markup) + #Now we know that chunk is either a string, or None. + if hasattr(matchAgainst, 'match'): + # It's a regexp object. + result = markup and matchAgainst.search(markup) + elif isList(matchAgainst): + result = markup in matchAgainst + elif hasattr(matchAgainst, 'items'): + result = markup.has_key(matchAgainst) + elif matchAgainst and isString(markup): + if isinstance(markup, unicode): + matchAgainst = unicode(matchAgainst) + else: + matchAgainst = str(matchAgainst) + + if not result: + result = matchAgainst == markup + return result + +class ResultSet(list): + """A ResultSet is just a list that keeps track of the SoupStrainer + that created it.""" + def __init__(self, source): + list.__init__([]) + self.source = source + +# Now, some helper functions. + +def isList(l): + """Convenience method that works with all 2.x versions of Python + to determine whether or not something is listlike.""" + return hasattr(l, '__iter__') \ + or (type(l) in (types.ListType, types.TupleType)) + +def isString(s): + """Convenience method that works with all 2.x versions of Python + to determine whether or not something is stringlike.""" + try: + return isinstance(s, unicode) or isintance(s, basestring) + except NameError: + return isinstance(s, str) + +def buildTagMap(default, *args): + """Turns a list of maps, lists, or scalars into a single map. + Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and + NESTING_RESET_TAGS maps out of lists and partial maps.""" + built = {} + for portion in args: + if hasattr(portion, 'items'): + #It's a map. Merge it. + for k,v in portion.items(): + built[k] = v + elif isList(portion): + #It's a list. Map each item to the default. + for k in portion: + built[k] = default + else: + #It's a scalar. Map it to the default. + built[portion] = default + return built + +# Now, the parser classes. + +class BeautifulStoneSoup(Tag, SGMLParser): + + """This class contains the basic parser and search code. It defines + a parser that knows nothing about tag behavior except for the + following: + + You can't close a tag without closing all the tags it encloses. + That is, "" actually means + "". + + [Another possible explanation is "", but since + this class defines no SELF_CLOSING_TAGS, it will never use that + explanation.] + + This class is useful for parsing XML or made-up markup languages, + or when BeautifulSoup makes an assumption counter to what you were + expecting.""" + + XML_ENTITY_LIST = {} + for i in Tag.XML_SPECIAL_CHARS_TO_ENTITIES.values(): + XML_ENTITY_LIST[i] = True + + SELF_CLOSING_TAGS = {} + NESTABLE_TAGS = {} + RESET_NESTING_TAGS = {} + QUOTE_TAGS = {} + + MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'), + lambda x: x.group(1) + ' />'), + (re.compile(']*)>'), + lambda x: '') + ] + + ROOT_TAG_NAME = u'[document]' + + HTML_ENTITIES = "html" + XML_ENTITIES = "xml" + + def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None, + markupMassage=True, smartQuotesTo=XML_ENTITIES, + convertEntities=None, selfClosingTags=None): + """The Soup object is initialized as the 'root tag', and the + provided markup (which can be a string or a file-like object) + is fed into the underlying parser. + + sgmllib will process most bad HTML, and the BeautifulSoup + class has some tricks for dealing with some HTML that kills + sgmllib, but Beautiful Soup can nonetheless choke or lose data + if your data uses self-closing tags or declarations + incorrectly. + + By default, Beautiful Soup uses regexes to sanitize input, + avoiding the vast majority of these problems. If the problems + don't apply to you, pass in False for markupMassage, and + you'll get better performance. + + The default parser massage techniques fix the two most common + instances of invalid HTML that choke sgmllib: + +
(No space between name of closing tag and tag close) + (Extraneous whitespace in declaration) + + You can pass in a custom list of (RE object, replace method) + tuples to get Beautiful Soup to scrub your input the way you + want.""" + + self.parseOnlyThese = parseOnlyThese + self.fromEncoding = fromEncoding + self.smartQuotesTo = smartQuotesTo + self.convertEntities = convertEntities + if self.convertEntities: + # It doesn't make sense to convert encoded characters to + # entities even while you're converting entities to Unicode. + # Just convert it all to Unicode. + self.smartQuotesTo = None + self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags) + SGMLParser.__init__(self) + + if hasattr(markup, 'read'): # It's a file-type object. + markup = markup.read() + self.markup = markup + self.markupMassage = markupMassage + try: + self._feed() + except StopParsing: + pass + self.markup = None # The markup can now be GCed + + def _feed(self, inDocumentEncoding=None): + # Convert the document to Unicode. + markup = self.markup + if isinstance(markup, unicode): + if not hasattr(self, 'originalEncoding'): + self.originalEncoding = None + else: + dammit = UnicodeDammit\ + (markup, [self.fromEncoding, inDocumentEncoding], + smartQuotesTo=self.smartQuotesTo) + markup = dammit.unicode + self.originalEncoding = dammit.originalEncoding + if markup: + if self.markupMassage: + if not isList(self.markupMassage): + self.markupMassage = self.MARKUP_MASSAGE + for fix, m in self.markupMassage: + markup = fix.sub(m, markup) + self.reset() + + SGMLParser.feed(self, markup) + # Close out any unfinished strings and close all the open tags. + self.endData() + while self.currentTag.name != self.ROOT_TAG_NAME: + self.popTag() + + def __getattr__(self, methodName): + """This method routes method call requests to either the SGMLParser + superclass or the Tag superclass, depending on the method name.""" + #print "__getattr__ called on %s.%s" % (self.__class__, methodName) + + if methodName.find('start_') == 0 or methodName.find('end_') == 0 \ + or methodName.find('do_') == 0: + return SGMLParser.__getattr__(self, methodName) + elif methodName.find('__') != 0: + return Tag.__getattr__(self, methodName) + else: + raise AttributeError + + def isSelfClosingTag(self, name): + """Returns true iff the given string is the name of a + self-closing tag according to this parser.""" + return self.SELF_CLOSING_TAGS.has_key(name) \ + or self.instanceSelfClosingTags.has_key(name) + + def reset(self): + Tag.__init__(self, self, self.ROOT_TAG_NAME) + self.hidden = 1 + SGMLParser.reset(self) + self.currentData = [] + self.currentTag = None + self.tagStack = [] + self.quoteStack = [] + self.pushTag(self) + + def popTag(self): + tag = self.tagStack.pop() + # Tags with just one string-owning child get the child as a + # 'string' property, so that soup.tag.string is shorthand for + # soup.tag.contents[0] + if len(self.currentTag.contents) == 1 and \ + isinstance(self.currentTag.contents[0], NavigableString): + self.currentTag.string = self.currentTag.contents[0] + + #print "Pop", tag.name + if self.tagStack: + self.currentTag = self.tagStack[-1] + return self.currentTag + + def pushTag(self, tag): + #print "Push", tag.name + if self.currentTag: + self.currentTag.append(tag) + self.tagStack.append(tag) + self.currentTag = self.tagStack[-1] + + def endData(self, containerClass=NavigableString): + if self.currentData: + currentData = ''.join(self.currentData) + if not currentData.strip(): + if '\n' in currentData: + currentData = '\n' + else: + currentData = ' ' + self.currentData = [] + if self.parseOnlyThese and len(self.tagStack) <= 1 and \ + (not self.parseOnlyThese.text or \ + not self.parseOnlyThese.search(currentData)): + return + o = containerClass(currentData) + o.setup(self.currentTag, self.previous) + if self.previous: + self.previous.next = o + self.previous = o + self.currentTag.contents.append(o) + + + def _popToTag(self, name, inclusivePop=True): + """Pops the tag stack up to and including the most recent + instance of the given tag. If inclusivePop is false, pops the tag + stack up to but *not* including the most recent instqance of + the given tag.""" + #print "Popping to %s" % name + if name == self.ROOT_TAG_NAME: + return + + numPops = 0 + mostRecentTag = None + for i in range(len(self.tagStack)-1, 0, -1): + if name == self.tagStack[i].name: + numPops = len(self.tagStack)-i + break + if not inclusivePop: + numPops = numPops - 1 + + for i in range(0, numPops): + mostRecentTag = self.popTag() + return mostRecentTag + + def _smartPop(self, name): + + """We need to pop up to the previous tag of this type, unless + one of this tag's nesting reset triggers comes between this + tag and the previous tag of this type, OR unless this tag is a + generic nesting trigger and another generic nesting trigger + comes between this tag and the previous tag of this type. + + Examples: +

FooBar

should pop to 'p', not 'b'. +

FooBar

should pop to 'table', not 'p'. +

Foo

Bar

should pop to 'tr', not 'p'. +

FooBar

should pop to 'p', not 'b'. + +

    • *
    • * should pop to 'ul', not the first 'li'. +
  • ** should pop to 'table', not the first 'tr' + tag should + implicitly close the previous tag within the same
    ** should pop to 'tr', not the first 'td' + """ + + nestingResetTriggers = self.NESTABLE_TAGS.get(name) + isNestable = nestingResetTriggers != None + isResetNesting = self.RESET_NESTING_TAGS.has_key(name) + popTo = None + inclusive = True + for i in range(len(self.tagStack)-1, 0, -1): + p = self.tagStack[i] + if (not p or p.name == name) and not isNestable: + #Non-nestable tags get popped to the top or to their + #last occurance. + popTo = name + break + if (nestingResetTriggers != None + and p.name in nestingResetTriggers) \ + or (nestingResetTriggers == None and isResetNesting + and self.RESET_NESTING_TAGS.has_key(p.name)): + + #If we encounter one of the nesting reset triggers + #peculiar to this tag, or we encounter another tag + #that causes nesting to reset, pop up to but not + #including that tag. + popTo = p.name + inclusive = False + break + p = p.parent + if popTo: + self._popToTag(popTo, inclusive) + + def unknown_starttag(self, name, attrs, selfClosing=0): + #print "Start tag %s: %s" % (name, attrs) + if self.quoteStack: + #This is not a real tag. + #print "<%s> is not real!" % name + attrs = ''.join(map(lambda(x, y): ' %s="%s"' % (x, y), attrs)) + self.handle_data('<%s%s>' % (name, attrs)) + return + self.endData() + + if not self.isSelfClosingTag(name) and not selfClosing: + self._smartPop(name) + + if self.parseOnlyThese and len(self.tagStack) <= 1 \ + and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)): + return + + tag = Tag(self, name, attrs, self.currentTag, self.previous) + if self.previous: + self.previous.next = tag + self.previous = tag + self.pushTag(tag) + if selfClosing or self.isSelfClosingTag(name): + self.popTag() + if name in self.QUOTE_TAGS: + #print "Beginning quote (%s)" % name + self.quoteStack.append(name) + self.literal = 1 + return tag + + def unknown_endtag(self, name): + #print "End tag %s" % name + if self.quoteStack and self.quoteStack[-1] != name: + #This is not a real end tag. + #print " is not real!" % name + self.handle_data('' % name) + return + self.endData() + self._popToTag(name) + if self.quoteStack and self.quoteStack[-1] == name: + self.quoteStack.pop() + self.literal = (len(self.quoteStack) > 0) + + def handle_data(self, data): + self.currentData.append(data) + + def _toStringSubclass(self, text, subclass): + """Adds a certain piece of text to the tree as a NavigableString + subclass.""" + self.endData() + self.handle_data(text) + self.endData(subclass) + + def handle_pi(self, text): + """Handle a processing instruction as a ProcessingInstruction + object, possibly one with a %SOUP-ENCODING% slot into which an + encoding will be plugged later.""" + if text[:3] == "xml": + text = "xml version='1.0' encoding='%SOUP-ENCODING%'" + self._toStringSubclass(text, ProcessingInstruction) + + def handle_comment(self, text): + "Handle comments as Comment objects." + self._toStringSubclass(text, Comment) + + def handle_charref(self, ref): + "Handle character references as data." + if self.convertEntities in [self.HTML_ENTITIES, + self.XML_ENTITIES]: + data = unichr(int(ref)) + else: + data = '&#%s;' % ref + self.handle_data(data) + + def handle_entityref(self, ref): + """Handle entity references as data, possibly converting known + HTML entity references to the corresponding Unicode + characters.""" + data = None + if self.convertEntities == self.HTML_ENTITIES or \ + (self.convertEntities == self.XML_ENTITIES and \ + self.XML_ENTITY_LIST.get(ref)): + try: + data = unichr(name2codepoint[ref]) + except KeyError: + pass + if not data: + data = '&%s;' % ref + self.handle_data(data) + + def handle_decl(self, data): + "Handle DOCTYPEs and the like as Declaration objects." + self._toStringSubclass(data, Declaration) + + def parse_declaration(self, i): + """Treat a bogus SGML declaration as raw data. Treat a CDATA + declaration as a CData object.""" + j = None + if self.rawdata[i:i+9] == '', i) + if k == -1: + k = len(self.rawdata) + data = self.rawdata[i+9:k] + j = k+3 + self._toStringSubclass(data, CData) + else: + try: + j = SGMLParser.parse_declaration(self, i) + except SGMLParseError: + toHandle = self.rawdata[i:] + self.handle_data(toHandle) + j = i + len(toHandle) + return j + +class BeautifulSoup(BeautifulStoneSoup): + + """This parser knows the following facts about HTML: + + * Some tags have no closing tag and should be interpreted as being + closed as soon as they are encountered. + + * The text inside some tags (ie. 'script') may contain tags which + are not really part of the document and which should be parsed + as text, not tags. If you want to parse the text as tags, you can + always fetch it and parse it explicitly. + + * Tag nesting rules: + + Most tags can't be nested at all. For instance, the occurance of + a

    tag should implicitly close the previous

    tag. + +

    Para1

    Para2 + should be transformed into: +

    Para1

    Para2 + + Some tags can be nested arbitrarily. For instance, the occurance + of a

    tag should _not_ implicitly close the previous +
    tag. + + Alice said:
    Bob said:
    Blah + should NOT be transformed into: + Alice said:
    Bob said:
    Blah + + Some tags can be nested, but the nesting is reset by the + interposition of other tags. For instance, a
    , + but not close a tag in another table. + +
    BlahBlah + should be transformed into: +
    BlahBlah + but, + Blah
    Blah + should NOT be transformed into + Blah
    Blah + + Differing assumptions about tag nesting rules are a major source + of problems with the BeautifulSoup class. If BeautifulSoup is not + treating as nestable a tag your page author treats as nestable, + try ICantBelieveItsBeautifulSoup, MinimalSoup, or + BeautifulStoneSoup before writing your own subclass.""" + + def __init__(self, *args, **kwargs): + if not kwargs.has_key('smartQuotesTo'): + kwargs['smartQuotesTo'] = self.HTML_ENTITIES + BeautifulStoneSoup.__init__(self, *args, **kwargs) + + SELF_CLOSING_TAGS = buildTagMap(None, + ['br' , 'hr', 'input', 'img', 'meta', + 'spacer', 'link', 'frame', 'base']) + + QUOTE_TAGS = {'script': None} + + #According to the HTML standard, each of these inline tags can + #contain another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_INLINE_TAGS = ['span', 'font', 'q', 'object', 'bdo', 'sub', 'sup', + 'center'] + + #According to the HTML standard, these block tags can contain + #another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_BLOCK_TAGS = ['blockquote', 'div', 'fieldset', 'ins', 'del'] + + #Lists can contain other lists, but there are restrictions. + NESTABLE_LIST_TAGS = { 'ol' : [], + 'ul' : [], + 'li' : ['ul', 'ol'], + 'dl' : [], + 'dd' : ['dl'], + 'dt' : ['dl'] } + + #Tables can contain other tables, but there are restrictions. + NESTABLE_TABLE_TAGS = {'table' : [], + 'tr' : ['table', 'tbody', 'tfoot', 'thead'], + 'td' : ['tr'], + 'th' : ['tr'], + 'thead' : ['table'], + 'tbody' : ['table'], + 'tfoot' : ['table'], + } + + NON_NESTABLE_BLOCK_TAGS = ['address', 'form', 'p', 'pre'] + + #If one of these tags is encountered, all tags up to the next tag of + #this type are popped. + RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript', + NON_NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, + NESTABLE_TABLE_TAGS) + + NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) + + # Used to detect the charset in a META tag; see start_meta + CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)") + + def start_meta(self, attrs): + """Beautiful Soup can detect a charset included in a META tag, + try to convert the document to that charset, and re-parse the + document from the beginning.""" + httpEquiv = None + contentType = None + contentTypeIndex = None + tagNeedsEncodingSubstitution = False + + for i in range(0, len(attrs)): + key, value = attrs[i] + key = key.lower() + if key == 'http-equiv': + httpEquiv = value + elif key == 'content': + contentType = value + contentTypeIndex = i + + if httpEquiv and contentType: # It's an interesting meta tag. + match = self.CHARSET_RE.search(contentType) + if match: + if getattr(self, 'declaredHTMLEncoding') or \ + (self.originalEncoding == self.fromEncoding): + # This is our second pass through the document, or + # else an encoding was specified explicitly and it + # worked. Rewrite the meta tag. + newAttr = self.CHARSET_RE.sub\ + (lambda(match):match.group(1) + + "%SOUP-ENCODING%", value) + attrs[contentTypeIndex] = (attrs[contentTypeIndex][0], + newAttr) + tagNeedsEncodingSubstitution = True + else: + # This is our first pass through the document. + # Go through it again with the new information. + newCharset = match.group(3) + if newCharset and newCharset != self.originalEncoding: + self.declaredHTMLEncoding = newCharset + self._feed(self.declaredHTMLEncoding) + raise StopParsing + tag = self.unknown_starttag("meta", attrs) + if tag and tagNeedsEncodingSubstitution: + tag.containsSubstitutions = True + +class StopParsing(Exception): + pass + +class ICantBelieveItsBeautifulSoup(BeautifulSoup): + + """The BeautifulSoup class is oriented towards skipping over + common HTML errors like unclosed tags. However, sometimes it makes + errors of its own. For instance, consider this fragment: + + FooBar + + This is perfectly valid (if bizarre) HTML. However, the + BeautifulSoup class will implicitly close the first b tag when it + encounters the second 'b'. It will think the author wrote + "FooBar", and didn't close the first 'b' tag, because + there's no real-world reason to bold something that's already + bold. When it encounters '' it will close two more 'b' + tags, for a grand total of three tags closed instead of two. This + can throw off the rest of your document structure. The same is + true of a number of other tags, listed below. + + It's much more common for someone to forget to close a 'b' tag + than to actually use nested 'b' tags, and the BeautifulSoup class + handles the common case. This class handles the not-co-common + case: where you can't believe someone wrote what they did, but + it's valid HTML and BeautifulSoup screwed up by assuming it + wouldn't be.""" + + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \ + ['em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong', + 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b', + 'big'] + + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ['noscript'] + + NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS) + +class MinimalSoup(BeautifulSoup): + """The MinimalSoup class is for parsing HTML that contains + pathologically bad markup. It makes no assumptions about tag + nesting, but it does know which tags are self-closing, that +