summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MANIFEST.in2
-rw-r--r--NEWS.txt15
-rw-r--r--bs4/testing.py320
-rw-r--r--bs4/tests/test_html5lib.py324
-rw-r--r--bs4/tests/test_htmlparser.py573
-rw-r--r--bs4/tests/test_lxml.py234
-rw-r--r--bs4/tests/test_soup.py45
-rw-r--r--scripts/demonstrate_parser_differences.py95
-rw-r--r--scripts/demonstration_markup.txt34
9 files changed, 534 insertions, 1108 deletions
diff --git a/MANIFEST.in b/MANIFEST.in
index fe99812..b5504e6 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,3 +3,5 @@ include doc/Makefile
include doc/source/*.py
include doc/source/*.rst
include doc/source/*.jpg
+include scripts/*.py
+include scripts/*.txt
diff --git a/NEWS.txt b/NEWS.txt
index 54caf6a..cd5d305 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -1,4 +1,4 @@
-= 4.0.0b7 () =
+= 4.0.0b7 (20110223) =
* Upon decoding to string, any characters that can't be represented in
your chosen encoding will be converted into numeric XML entity
@@ -14,6 +14,19 @@
* It's now possible to deepcopy a BeautifulSoup object created with
Python's built-in HTML parser.
+* About 100 unit tests that "test" the behavior of various parsers on
+ invalid markup have been removed. Legitimate changes to those
+ parsers caused these tests to fail, indicating that perhaps
+ Beautiful Soup should not test the behavior of foreign
+ libraries.
+
+ The problematic unit tests have been reformulated as informational
+ comparisons generated by the script
+ scripts/demonstrate_parser_differences.py.
+
+ This makes Beautiful Soup compatible with html5lib version 0.95 and
+ future versions of HTMLParser.
+
= 4.0.0b6 (20110216) =
* Multi-valued attributes like "class" always have a list of values,
diff --git a/bs4/testing.py b/bs4/testing.py
index e5e8c93..cc30e17 100644
--- a/bs4/testing.py
+++ b/bs4/testing.py
@@ -1,16 +1,19 @@
"""Helper classes for tests."""
+import copy
import functools
import unittest
from unittest import TestCase
from bs4 import BeautifulSoup
-from bs4.element import Comment, SoupStrainer
-try:
- from bs4.builder import LXMLTreeBuilder
- default_builder = LXMLTreeBuilder
-except ImportError, e:
- from bs4.builder import HTMLParserTreeBuilder
- default_builder = HTMLParserTreeBuilder
+from bs4.element import (
+ Comment,
+ Doctype,
+ SoupStrainer,
+)
+
+from bs4.builder import HTMLParserTreeBuilder
+default_builder = HTMLParserTreeBuilder
+
class SoupTest(unittest.TestCase):
@@ -38,6 +41,309 @@ class SoupTest(unittest.TestCase):
self.assertEqual(obj.decode(), self.document_for(compare_parsed_to))
+
+class HTMLTreeBuilderSmokeTest(object):
+
+ """A basic test of a treebuilder's competence.
+
+ Any HTML treebuilder, present or future, should be able to pass
+ these tests. With invalid markup, there's room for interpretation,
+ and different parsers can handle it differently. But with the
+ markup in these tests, there's not much room for interpretation.
+ """
+
+ def assertDoctypeHandled(self, doctype_fragment):
+ """Assert that a given doctype string is handled correctly."""
+ doctype_str, soup = self._document_with_doctype(doctype_fragment)
+
+ # Make sure a Doctype object was created.
+ doctype = soup.contents[0]
+ self.assertEqual(doctype.__class__, Doctype)
+ self.assertEqual(doctype, doctype_fragment)
+ self.assertEqual(str(soup)[:len(doctype_str)], doctype_str)
+
+ # Make sure that the doctype was correctly associated with the
+ # parse tree and that the rest of the document parsed.
+ self.assertEqual(soup.p.contents[0], 'foo')
+
+ def _document_with_doctype(self, doctype_fragment):
+ """Generate and parse a document with the given doctype."""
+ doctype = '<!DOCTYPE %s>' % doctype_fragment
+ markup = doctype + '\n<p>foo</p>'
+ soup = self.soup(markup)
+ return doctype, soup
+
+ def test_normal_doctypes(self):
+ """Make sure normal, everyday HTML doctypes are handled correctly."""
+ self.assertDoctypeHandled("html")
+ self.assertDoctypeHandled(
+ 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"')
+
+ def test_namespaced_system_doctype(self):
+ # We can handle a namespaced doctype with a system ID.
+ self.assertDoctypeHandled('xsl:stylesheet SYSTEM "htmlent.dtd"')
+
+ def test_namespaced_public_doctype(self):
+ # Test a namespaced doctype with a public id.
+ self.assertDoctypeHandled('xsl:stylesheet PUBLIC "htmlent.dtd"')
+
+ def test_deepcopy(self):
+ """Make sure you can copy the tree builder.
+
+ This is important because the builder is part of a
+ BeautifulSoup object, and we want to be able to copy that.
+ """
+ copy.deepcopy(self.default_builder)
+
+ def test_p_tag_is_never_empty_element(self):
+ """A <p> tag is never designated as an empty-element tag.
+
+ Even if the markup shows it as an empty-element tag, it
+ shouldn't be presented that way.
+ """
+ soup = self.soup("<p/>")
+ self.assertFalse(soup.p.is_empty_element)
+ self.assertEqual(str(soup.p), "<p></p>")
+
+ def test_unclosed_tags_get_closed(self):
+ """A tag that's not closed by the end of the document should be closed.
+
+ This applies to all tags except empty-element tags.
+ """
+ self.assertSoupEquals("<p>", "<p></p>")
+ self.assertSoupEquals("<b>", "<b></b>")
+
+ self.assertSoupEquals("<br>", "<br/>")
+
+ def test_br_is_always_empty_element_tag(self):
+ """A <br> tag is designated as an empty-element tag.
+
+ Some parsers treat <br></br> as one <br/> tag, some parsers as
+ two tags, but it should always be an empty-element tag.
+ """
+ soup = self.soup("<br></br>")
+ self.assertTrue(soup.br.is_empty_element)
+ self.assertEqual(str(soup.br), "<br/>")
+
+ def test_comment(self):
+ # Comments are represented as Comment objects.
+ markup = "<p>foo<!--foobar-->baz</p>"
+ self.assertSoupEquals(markup)
+
+ soup = self.soup(markup)
+ comment = soup.find(text="foobar")
+ self.assertEqual(comment.__class__, Comment)
+
+ def test_preserved_whitespace_in_pre_and_textarea(self):
+ """Whitespace must be preserved in <pre> and <textarea> tags."""
+ self.assertSoupEquals("<pre> </pre>")
+ self.assertSoupEquals("<textarea> woo </textarea>")
+
+ def test_nested_inline_elements(self):
+ """Inline elements can be nested indefinitely."""
+ b_tag = "<b>Inside a B tag</b>"
+ self.assertSoupEquals(b_tag)
+
+ nested_b_tag = "<p>A <i>nested <b>tag</b></i></p>"
+ self.assertSoupEquals(nested_b_tag)
+
+ double_nested_b_tag = "<p>A <a>doubly <i>nested <b>tag</b></i></a></p>"
+ self.assertSoupEquals(nested_b_tag)
+
+ def test_nested_block_level_elements(self):
+ """Block elements can be nested."""
+ soup = self.soup('<blockquote><p><b>Foo</b></p></blockquote>')
+ blockquote = soup.blockquote
+ self.assertEqual(blockquote.p.b.string, 'Foo')
+ self.assertEqual(blockquote.b.string, 'Foo')
+
+ def test_correctly_nested_tables(self):
+ """One table can go inside another one."""
+ markup = ('<table id="1">'
+ '<tr>'
+ "<td>Here's another table:"
+ '<table id="2">'
+ '<tr><td>foo</td></tr>'
+ '</table></td>')
+
+ self.assertSoupEquals(
+ markup,
+ '<table id="1"><tr><td>Here\'s another table:'
+ '<table id="2"><tr><td>foo</td></tr></table>'
+ '</td></tr></table>')
+
+ self.assertSoupEquals(
+ "<table><thead><tr><td>Foo</td></tr></thead>"
+ "<tbody><tr><td>Bar</td></tr></tbody>"
+ "<tfoot><tr><td>Baz</td></tr></tfoot></table>")
+
+ def test_angle_brackets_in_attribute_values_are_escaped(self):
+ self.assertSoupEquals('<a b="<a>"></a>', '<a b="&lt;a&gt;"></a>')
+
+ def test_entities_in_attributes_converted_to_unicode(self):
+ expect = u'<p id="pi\N{LATIN SMALL LETTER N WITH TILDE}ata"></p>'
+ self.assertSoupEquals('<p id="pi&#241;ata"></p>', expect)
+ self.assertSoupEquals('<p id="pi&#xf1;ata"></p>', expect)
+ self.assertSoupEquals('<p id="pi&ntilde;ata"></p>', expect)
+
+ def test_entities_in_text_converted_to_unicode(self):
+ expect = u'<p>pi\N{LATIN SMALL LETTER N WITH TILDE}ata</p>'
+ self.assertSoupEquals("<p>pi&#241;ata</p>", expect)
+ self.assertSoupEquals("<p>pi&#xf1;ata</p>", expect)
+ self.assertSoupEquals("<p>pi&ntilde;ata</p>", expect)
+
+ def test_out_of_range_entity(self):
+ expect = u"\N{REPLACEMENT CHARACTER}"
+ self.assertSoupEquals("&#10000000000000;", expect)
+ self.assertSoupEquals("&#x10000000000000;", expect)
+ self.assertSoupEquals("&#1000000000;", expect)
+
+ #
+ # Generally speaking, tests below this point are more tests of
+ # Beautiful Soup than tests of the tree builders. But parsers are
+ # weird, so we run these tests separately for every tree builder
+ # to detect any differences between them.
+ #
+
+ def test_soupstrainer(self):
+ """Parsers should be able to work with SoupStrainers."""
+ strainer = SoupStrainer("b")
+ soup = self.soup("A <b>bold</b> <meta/> <i>statement</i>",
+ parse_only=strainer)
+ self.assertEqual(soup.decode(), "<b>bold</b>")
+
+ def test_single_quote_attribute_values_become_double_quotes(self):
+ self.assertSoupEquals("<foo attr='bar'></foo>",
+ '<foo attr="bar"></foo>')
+
+ def test_attribute_values_with_nested_quotes_are_left_alone(self):
+ text = """<foo attr='bar "brawls" happen'>a</foo>"""
+ self.assertSoupEquals(text)
+
+ def test_attribute_values_with_double_nested_quotes_get_quoted(self):
+ text = """<foo attr='bar "brawls" happen'>a</foo>"""
+ soup = self.soup(text)
+ soup.foo['attr'] = 'Brawls happen at "Bob\'s Bar"'
+ self.assertSoupEquals(
+ soup.foo.decode(),
+ """<foo attr="Brawls happen at &quot;Bob\'s Bar&quot;">a</foo>""")
+
+ def test_ampersand_in_attribute_value_gets_escaped(self):
+ self.assertSoupEquals('<this is="really messed up & stuff"></this>',
+ '<this is="really messed up &amp; stuff"></this>')
+
+ self.assertSoupEquals(
+ '<a href="http://example.org?a=1&b=2;3">foo</a>',
+ '<a href="http://example.org?a=1&amp;b=2;3">foo</a>')
+
+ def test_escaped_ampersand_in_attribute_value_is_left_alone(self):
+ self.assertSoupEquals('<a href="http://example.org?a=1&amp;b=2;3"></a>')
+
+ def test_entities_in_strings_converted_during_parsing(self):
+ # Both XML and HTML entities are converted to Unicode characters
+ # during parsing.
+ text = "<p>&lt;&lt;sacr&eacute;&#32;bleu!&gt;&gt;</p>"
+ expected = u"<p>&lt;&lt;sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!&gt;&gt;</p>"
+ self.assertSoupEquals(text, expected)
+
+ def test_smart_quotes_converted_on_the_way_in(self):
+ # Microsoft smart quotes are converted to Unicode characters during
+ # parsing.
+ quote = b"<p>\x91Foo\x92</p>"
+ soup = self.soup(quote)
+ self.assertEqual(
+ soup.p.string,
+ u"\N{LEFT SINGLE QUOTATION MARK}Foo\N{RIGHT SINGLE QUOTATION MARK}")
+
+ def test_non_breaking_spaces_converted_on_the_way_in(self):
+ soup = self.soup("<a>&nbsp;&nbsp;</a>")
+ self.assertEqual(soup.a.string, u"\N{NO-BREAK SPACE}" * 2)
+
+ def test_entities_converted_on_the_way_out(self):
+ text = "<p>&lt;&lt;sacr&eacute;&#32;bleu!&gt;&gt;</p>"
+ expected = u"<p>&lt;&lt;sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!&gt;&gt;</p>".encode("utf-8")
+ soup = self.soup(text)
+ self.assertEqual(soup.p.encode("utf-8"), expected)
+
+ def test_real_iso_latin_document(self):
+ # Smoke test of interrelated functionality, using an
+ # easy-to-understand document.
+
+ # Here it is in Unicode. Note that it claims to be in ISO-Latin-1.
+ unicode_html = u'<html><head><meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type"/></head><body><p>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</p></body></html>'
+
+ # That's because we're going to encode it into ISO-Latin-1, and use
+ # that to test.
+ iso_latin_html = unicode_html.encode("iso-8859-1")
+
+ # Parse the ISO-Latin-1 HTML.
+ soup = self.soup(iso_latin_html)
+ # Encode it to UTF-8.
+ result = soup.encode("utf-8")
+
+ # What do we expect the result to look like? Well, it would
+ # look like unicode_html, except that the META tag would say
+ # UTF-8 instead of ISO-Latin-1.
+ expected = unicode_html.replace("ISO-Latin-1", "utf-8")
+
+ # And, of course, it would be in UTF-8, not Unicode.
+ expected = expected.encode("utf-8")
+
+ # Ta-da!
+ self.assertEqual(result, expected)
+
+ def test_real_shift_jis_document(self):
+ # Smoke test to make sure the parser can handle a document in
+ # Shift-JIS encoding, without choking.
+ shift_jis_html = (
+ b'<html><head></head><body><pre>'
+ b'\x82\xb1\x82\xea\x82\xcdShift-JIS\x82\xc5\x83R\x81[\x83f'
+ b'\x83B\x83\x93\x83O\x82\xb3\x82\xea\x82\xbd\x93\xfa\x96{\x8c'
+ b'\xea\x82\xcc\x83t\x83@\x83C\x83\x8b\x82\xc5\x82\xb7\x81B'
+ b'</pre></body></html>')
+ unicode_html = shift_jis_html.decode("shift-jis")
+ soup = self.soup(unicode_html)
+
+ # Make sure the parse tree is correctly encoded to various
+ # encodings.
+ self.assertEqual(soup.encode("utf-8"), unicode_html.encode("utf-8"))
+ self.assertEqual(soup.encode("euc_jp"), unicode_html.encode("euc_jp"))
+
+ def test_real_hebrew_document(self):
+ # A real-world test to make sure we can convert ISO-8859-9 (a
+ # Hebrew encoding) to UTF-8.
+ hebrew_document = b'<html><head><title>Hebrew (ISO 8859-8) in Visual Directionality</title></head><body><h1>Hebrew (ISO 8859-8) in Visual Directionality</h1>\xed\xe5\xec\xf9</body></html>'
+ soup = self.soup(
+ hebrew_document, from_encoding="iso8859-8")
+ self.assertEqual(soup.original_encoding, 'iso8859-8')
+ self.assertEqual(
+ soup.encode('utf-8'),
+ hebrew_document.decode("iso8859-8").encode("utf-8"))
+
+ def test_meta_tag_reflects_current_encoding(self):
+ # Here's the <meta> tag saying that a document is
+ # encoded in Shift-JIS.
+ meta_tag = ('<meta content="text/html; charset=x-sjis" '
+ 'http-equiv="Content-type"/>')
+
+ # Here's a document incorporating that meta tag.
+ shift_jis_html = (
+ '<html><head>\n%s\n'
+ '<meta http-equiv="Content-language" content="ja"/>'
+ '</head><body>Shift-JIS markup goes here.') % meta_tag
+ soup = self.soup(shift_jis_html)
+
+ # Parse the document, and the charset is replaced with a
+ # generic value.
+ parsed_meta = soup.find('meta', {'http-equiv': 'Content-type'})
+ self.assertEqual(parsed_meta['content'],
+ 'text/html; charset=%SOUP-ENCODING%')
+ self.assertEqual(parsed_meta.contains_substitutions, True)
+
+ # For the rest of the story, see TestSubstitutions in
+ # test_tree.py.
+
+
def skipIf(condition, reason):
def nothing(test, *args, **kwargs):
return None
diff --git a/bs4/tests/test_html5lib.py b/bs4/tests/test_html5lib.py
index 3f00d52..f1edddf 100644
--- a/bs4/tests/test_html5lib.py
+++ b/bs4/tests/test_html5lib.py
@@ -1,18 +1,22 @@
+"""Tests to ensure that the html5lib tree builder generates good trees."""
+
try:
from bs4.builder import HTML5TreeBuilder
HTML5LIB_PRESENT = True
except ImportError, e:
HTML5LIB_PRESENT = False
-from bs4.element import Comment, SoupStrainer
-import test_htmlparser
-import unittest
-from bs4.testing import skipIf
+from bs4.element import SoupStrainer
+from bs4.testing import (
+ HTMLTreeBuilderSmokeTest,
+ SoupTest,
+ skipIf,
+)
@skipIf(
not HTML5LIB_PRESENT,
"html5lib seems not to be present, not testing its tree builder.")
-class TestHTML5Builder(test_htmlparser.TestHTMLParserTreeBuilder):
- """See `BuilderSmokeTest`."""
+class HTML5LibBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest):
+ """See ``HTMLTreeBuilderSmokeTest``."""
@property
def default_builder(self):
@@ -27,15 +31,8 @@ class TestHTML5Builder(test_htmlparser.TestHTMLParserTreeBuilder):
self.assertEqual(
soup.decode(), self.document_for(markup))
- def test_bare_string(self):
- # A bare string is turned into some kind of HTML document or
- # fragment recognizable as the original string.
- #
- # In this case, html5lib puts a <p> tag around the bare string.
- self.assertSoupEquals(
- "A bare string", "A bare string")
-
def test_correctly_nested_tables(self):
+ """html5lib inserts <tbody> tags where other parsers don't."""
markup = ('<table id="1">'
'<tr>'
"<td>Here's another table:"
@@ -53,302 +50,3 @@ class TestHTML5Builder(test_htmlparser.TestHTMLParserTreeBuilder):
"<table><thead><tr><td>Foo</td></tr></thead>"
"<tbody><tr><td>Bar</td></tr></tbody>"
"<tfoot><tr><td>Baz</td></tr></tfoot></table>")
-
- def test_literal_in_textarea(self):
- markup = '<textarea>Junk like <b> tags and <&<&amp;</textarea>'
- soup = self.soup(markup)
- self.assertEqual(
- soup.textarea.contents, ["Junk like <b> tags and <&<&"])
-
- def test_collapsed_whitespace(self):
- """Whitespace is preserved even in tags that don't require it."""
- self.assertSoupEquals("<p> </p>")
- self.assertSoupEquals("<b> </b>")
-
- def test_cdata_where_its_ok(self):
- # In html5lib 0.9.0, all CDATA sections are converted into
- # comments. In a later version (unreleased as of this
- # writing), CDATA sections in tags like <svg> and <math> will
- # be preserved. BUT, I'm not sure how Beautiful Soup needs to
- # adjust to transform this preservation into the construction
- # of a BS CData object.
- markup = "<svg><![CDATA[foobar]]>"
-
- # Eventually we should be able to do a find(text="foobar") and
- # get a CData object.
- self.assertSoupEquals(markup, "<svg><!--[CDATA[foobar]]--></svg>")
-
- def test_entities_in_attribute_values_converted_during_parsing(self):
-
- # The numeric entity is recognized even without the closing
- # semicolon.
- text = '<x t="pi&#241ata">'
- expected = u"pi\N{LATIN SMALL LETTER N WITH TILDE}ata"
- soup = self.soup(text)
- self.assertEqual(soup.x['t'], expected)
-
- def test_naked_ampersands(self):
- # Ampersands are not treated as entities, unlike in html.parser.
- text = "<p>AT&T</p>"
- soup = self.soup(text)
- self.assertEqual(soup.p.string, "AT&T")
-
- def test_namespaced_system_doctype(self):
- # Test a namespaced doctype with a system id.
- self._test_doctype('xsl:stylesheet SYSTEM "htmlent.dtd"')
-
- def test_namespaced_public_doctype(self):
- # Test a namespaced doctype with a public id.
- self._test_doctype('xsl:stylesheet PUBLIC "htmlent.dtd"')
-
-
-@skipIf(
- not HTML5LIB_PRESENT,
- "html5lib seems not to be present, not testing it on invalid markup.")
-class TestHTML5BuilderInvalidMarkup(
- test_htmlparser.TestHTMLParserTreeBuilderInvalidMarkup):
- """See `BuilderInvalidMarkupSmokeTest`."""
-
- @property
- def default_builder(self):
- return HTML5TreeBuilder()
-
- def test_unclosed_block_level_elements(self):
- # The unclosed <b> tag is closed so that the block-level tag
- # can be closed, and another <b> tag is inserted after the
- # next block-level tag begins.
- self.assertSoupEquals(
- '<blockquote><p><b>Foo</blockquote><p>Bar',
- '<blockquote><p><b>Foo</b></p></blockquote><p><b>Bar</b></p>')
-
- def test_attribute_value_never_got_closed(self):
- markup = '<a href="http://foo.com/</a> and blah and blah'
- soup = self.soup(markup)
- self.assertEqual(
- soup.a['href'], "http://foo.com/</a> and blah and blah")
-
- def test_attribute_value_was_closed_by_subsequent_tag(self):
- markup = """<a href="foo</a>, </a><a href="bar">baz</a>"""
- soup = self.soup(markup)
- # The string between the first and second quotes was interpreted
- # as the value of the 'href' attribute.
- self.assertEqual(soup.a['href'], 'foo</a>, </a><a href=')
-
- #The string after the second quote (bar"), was treated as an
- #empty attribute called bar.
- self.assertEqual(soup.a['bar'], '')
- self.assertEqual(soup.a.string, "baz")
-
- def test_document_starts_with_bogus_declaration(self):
- soup = self.soup('<! Foo ><p>a</p>')
- # The declaration is ignored altogether.
- self.assertEqual(soup.encode(), b"<html><body><p>a</p></body></html>")
-
- def test_table_containing_bare_markup(self):
- # Markup should be in table cells, not directly in the table.
- self.assertSoupEquals("<table><div>Foo</div></table>",
- "<div>Foo</div><table></table>")
-
- def test_unclosed_a_tag(self):
- # n.b. the whitespace is important here.
- markup = """<div id="1">
- <a href="foo">
-</div>
-<div id="2">
- <div id="3">
- <a href="bar"></a>
- </div>
-</div>"""
-
- expect = """<div id="1">
- <a href="foo">
-</a></div><a href="foo">
-</a><div id="2"><a href="foo">
- </a><div id="3"><a href="foo">
- </a><a href="bar"></a>
- </div>
-</div>"""
- self.assertSoupEquals(markup, expect)
-
- def test_incorrectly_nested_tables(self):
- self.assertSoupEquals(
- '<table><tr><table><tr id="nested">',
- ('<table><tbody><tr></tr></tbody></table>'
- '<table><tbody><tr id="nested"></tr></tbody></table>'))
-
- def test_floating_text_in_table(self):
- self.assertSoupEquals(
- "<table><td></td>foo<td>bar</td></table>",
- "foo<table><tbody><tr><td></td><td>bar</td></tr></tbody></table>")
-
- def test_empty_element_tag_with_contents(self):
- self.assertSoupEquals("<br>foo</br>", "<br/>foo<br/>")
-
- def test_doctype_in_body(self):
- markup = "<p>one<!DOCTYPE foobar>two</p>"
- self.assertSoupEquals(markup, "<p>onetwo</p>")
-
- def test_cdata_where_it_doesnt_belong(self):
- # Random CDATA sections are converted into comments.
- markup = "<div><![CDATA[foo]]>"
- soup = self.soup(markup)
- data = soup.find(text="[CDATA[foo]]")
- self.assertEqual(data.__class__, Comment)
-
- def test_nonsensical_declaration(self):
- # Declarations that don't make any sense are turned into comments.
- soup = self.soup('<! Foo = -8><p>a</p>')
- self.assertEqual(str(soup),
- ("<!-- Foo = -8-->"
- "<html><head></head><body><p>a</p></body></html>"))
-
- soup = self.soup('<p>a</p><! Foo = -8>')
- self.assertEqual(str(soup),
- ("<html><head></head><body><p>a</p>"
- "<!-- Foo = -8--></body></html>"))
-
- def test_whitespace_in_doctype(self):
- # A declaration that has extra whitespace is turned into a comment.
- soup = self.soup((
- '<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">'
- '<p>foo</p>'))
- self.assertEqual(
- str(soup),
- ('<!-- DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"-->'
- '<html><head></head><body><p>foo</p></body></html>'))
-
- def test_incomplete_declaration(self):
- # An incomplete declaration is treated as a comment.
- markup = 'a<!b <p>c'
- self.assertSoupEquals(markup, "a<!--b &lt;p-->c")
-
- # Let's spell that out a little more explicitly.
- soup = self.soup(markup)
- str1, comment, str2 = soup.body.contents
- self.assertEqual(str1, 'a')
- self.assertEqual(comment.__class__, Comment)
- self.assertEqual(comment, 'b <p')
- self.assertEqual(str2, 'c')
-
- def test_document_starts_with_bogus_declaration(self):
- soup = self.soup('<! Foo >a')
- # 'Foo' becomes a comment that appears before the HTML.
- comment = soup.contents[0]
- self.assertTrue(isinstance(comment, Comment))
- self.assertEqual(comment, 'Foo')
-
- self.assertEqual(self.find(text="a") == "a")
-
- def test_attribute_value_was_closed_by_subsequent_tag(self):
- markup = """<a href="foo</a>, </a><a href="bar">baz</a>"""
- soup = self.soup(markup)
- # The string between the first and second quotes was interpreted
- # as the value of the 'href' attribute.
- self.assertEqual(soup.a['href'], 'foo</a>, </a><a href=')
-
- #The string after the second quote (bar"), was treated as an
- #empty attribute called bar".
- self.assertEqual(soup.a['bar"'], '')
- self.assertEqual(soup.a.string, "baz")
-
- def test_document_starts_with_bogus_declaration(self):
- soup = self.soup('<! Foo ><p>a</p>')
- # The declaration becomes a comment.
- comment = soup.contents[0]
- self.assertTrue(isinstance(comment, Comment))
- self.assertEqual(comment, ' Foo ')
- self.assertEqual(soup.p.string, 'a')
-
- def test_document_ends_with_incomplete_declaration(self):
- soup = self.soup('<p>a<!b')
- # This becomes a string 'a'. The incomplete declaration is ignored.
- # Compare html5lib, which turns it into a comment.
- s, comment = soup.p.contents
- self.assertEqual(s, 'a')
- self.assertTrue(isinstance(comment, Comment))
- self.assertEqual(comment, 'b')
-
- def test_entity_was_not_finished(self):
- soup = self.soup("<p>&lt;Hello&gt")
- # Compare html5lib, which completes the entity.
- self.assertEqual(soup.p.string, "<Hello>")
-
- def test_nonexistent_entity(self):
- soup = self.soup("<p>foo&#bar;baz</p>")
- self.assertEqual(soup.p.string, "foo&#bar;baz")
-
- # Compare a real entity.
- soup = self.soup("<p>foo&#100;baz</p>")
- self.assertEqual(soup.p.string, "foodbaz")
-
- def test_entity_out_of_range(self):
- # An entity that's out of range will be converted to
- # REPLACEMENT CHARACTER.
- soup = self.soup("<p>&#10000000000000;</p>")
- self.assertEqual(soup.p.string, u"\N{REPLACEMENT CHARACTER}")
-
- soup = self.soup("<p>&#x1000000000000;</p>")
- self.assertEqual(soup.p.string, u"\N{REPLACEMENT CHARACTER}")
-
- def test_incomplete_declaration(self):
- self.assertSoupEquals('a<!b <p>c', 'a<!--b &lt;p-->c')
-
- def test_nonsensical_declaration(self):
- soup = self.soup('<! Foo = -8><p>a</p>')
- self.assertEquals(
- soup.decode(),
- "<!-- Foo = -8--><html><head></head><body><p>a</p></body></html>")
-
- def test_unquoted_attribute_value(self):
- soup = self.soup('<a style={height:21px;}></a>')
- self.assertEqual(soup.a['style'], '{height:21px;}')
-
- def test_boolean_attribute_with_no_value(self):
- soup = self.soup("<table><td nowrap>foo</td></table>")
- self.assertEqual(soup.table.td['nowrap'], '')
-
- def test_cdata_where_it_doesnt_belong(self):
- #CDATA sections are ignored.
- markup = "<div><![CDATA[foo]]>"
- self.assertSoupEquals(markup, "<div><!--[CDATA[foo]]--></div>")
-
- def test_empty_element_tag_with_contents(self):
- self.assertSoupEquals("<br>foo</br>", "<br/>foo<br/>")
-
- def test_fake_self_closing_tag(self):
- # If a self-closing tag presents as a normal tag, the 'open'
- # tag is treated as an instance of the self-closing tag and
- # the 'close' tag is ignored.
- self.assertSoupEquals(
- "<item><link>http://foo.com/</link></item>",
- "<item><link/>http://foo.com/</item>")
-
- def test_paragraphs_containing_block_display_elements(self):
- markup = self.soup("<p>this is the definition:"
- "<dl><dt>first case</dt>")
- # The <p> tag is closed before the <dl> tag begins.
- self.assertEqual(markup.p.contents, ["this is the definition:"])
-
- def test_multiple_values_for_the_same_attribute(self):
- markup = '<b b="20" a="1" b="10" a="2" a="3" a="4"></b>'
- self.assertSoupEquals(markup, '<b a="1" b="20"></b>')
-
-
-@skipIf(
- not HTML5LIB_PRESENT,
- "html5lib seems not to be present, not testing it on encoding conversion.")
-class TestHTML5LibEncodingConversion(
- test_htmlparser.TestHTMLParserTreeBuilderEncodingConversion):
- @property
- def default_builder(self):
- return HTML5TreeBuilder()
-
- def test_real_hebrew_document(self):
- # A real-world test to make sure we can convert ISO-8859-8 (a
- # Hebrew encoding) to UTF-8.
- soup = self.soup(self.HEBREW_DOCUMENT,
- from_encoding="iso-8859-8")
- self.assertEqual(soup.original_encoding, 'iso8859-8')
- self.assertEqual(
- soup.encode('utf-8'),
- self.HEBREW_DOCUMENT.decode("iso-8859-8").encode("utf-8"))
diff --git a/bs4/tests/test_htmlparser.py b/bs4/tests/test_htmlparser.py
index d5b6ae1..bcb5ed2 100644
--- a/bs4/tests/test_htmlparser.py
+++ b/bs4/tests/test_htmlparser.py
@@ -1,574 +1,19 @@
-import copy
-from HTMLParser import HTMLParseError
-from bs4.element import Comment, Doctype, SoupStrainer
-from bs4.builder import HTMLParserTreeBuilder
-from bs4.element import CData
-from bs4.testing import SoupTest
-
-class TestHTMLParserTreeBuilder(SoupTest):
+"""Tests to ensure that the html.parser tree builder generates good
+trees."""
- """A smoke test for the built-in tree builder.
-
- Subclass this to test some other HTML tree builder. Subclasses of
- this test ensure that all of Beautiful Soup's tree builders
- generate more or less the same trees.
+from bs4.testing import SoupTest, HTMLTreeBuilderSmokeTest
+from bs4.builder import HTMLParserTreeBuilder
- It's okay for trees to differ--just override the appropriate test
- method to demonstrate how one tree builder differs from the
- default builder. But in general, all HTML tree builders should
- generate trees that make most of these tests pass.
- """
+class HTMLParserTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest):
@property
def default_builder(self):
return HTMLParserTreeBuilder()
- def test_bare_string(self):
- # A bare string is turned into some kind of HTML document or
- # fragment recognizable as the original string.
- #
- # HTMLParser does not modify the bare string at all.
- self.assertSoupEquals("A bare string")
-
- def test_cdata_where_its_ok(self):
- # HTMLParser recognizes CDATA sections and passes them through.
- markup = "<svg><![CDATA[foobar]]></svg>"
- self.assertSoupEquals(markup)
- soup = self.soup(markup)
- string = soup.svg.string
- self.assertEqual(string, "foobar")
- self.assertTrue(isinstance(string, CData))
-
- def test_hex_entities_in_text(self):
- # XXX This tests a workaround for a bug in HTMLParser.
- self.assertSoupEquals("<p>&#xf1;</p>", u"<p>\xf1</p>")
-
- def test_entities_in_attribute_values_converted_during_parsing(self):
-
- # The numeric entity isn't recognized without the closing
- # semicolon.
- text = '<x t="pi&#241ata">'
- expected = u"pi\N{LATIN SMALL LETTER N WITH TILDE}ata"
- soup = self.soup(text)
- self.assertEqual(soup.x['t'], "pi&#241ata")
-
- text = '<x t="pi&#241;ata">'
- expected = u"pi\N{LATIN SMALL LETTER N WITH TILDE}ata"
- soup = self.soup(text)
- self.assertEqual(soup.x['t'], u"pi\xf1ata")
-
- text = '<x t="pi&#xf1;ata">'
- soup = self.soup(text)
- self.assertEqual(soup.x['t'], expected)
-
- text = '<x t="sacr&eacute; bleu">'
- soup = self.soup(text)
- self.assertEqual(
- soup.x['t'],
- u"sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu")
-
- # This can cause valid HTML to become invalid.
- valid_url = '<a href="http://example.org?a=1&amp;b=2;3">foo</a>'
- soup = self.soup(valid_url)
- self.assertEqual(soup.a['href'], "http://example.org?a=1&b=2;3")
-
- # I think it would be very difficult to 'fix' these tests, judging
- # from my experience with previous versions of Beautiful Soup.
- def test_naked_ampersands(self):
- # Ampersands are treated as entities.
- text = "<p>AT&T</p>"
- soup = self.soup(text)
- self.assertEqual(soup.p.string, "AT&T;")
-
- def test_literal_in_textarea(self):
- # Anything inside a <textarea> is supposed to be treated as
- # the literal value of the field, (XXX citation
- # needed). html5lib does this correctly. But, HTMLParser does its
- # best to parse the contents of a <textarea> as HTML.
- text = '<textarea>Junk like <b> tags and <&<&amp;</textarea>'
- soup = self.soup(text)
- self.assertEqual(len(soup.textarea.contents), 2)
- self.assertEqual(soup.textarea.contents[0], u"Junk like ")
- self.assertEqual(soup.textarea.contents[1].name, 'b')
- self.assertEqual(soup.textarea.b.string, u" tags and <&<&")
-
- def test_literal_in_script(self):
- # Some versions of HTMLParser choke on markup like this:
- # if (i < 2) { alert("<b>foo</b>"); }
- # Some versions of HTMLParser don't.
- #
- # The easiest thing is to just not run this test for HTMLParser.
- pass
-
- # Namespaced doctypes cause an HTMLParseError
def test_namespaced_system_doctype(self):
- self.assertRaises(HTMLParseError, self._test_doctype,
- 'xsl:stylesheet SYSTEM "htmlent.dtd"')
+ # html.parser can't handle namespaced doctypes, so skip this one.
+ pass
def test_namespaced_public_doctype(self):
- self.assertRaises(HTMLParseError, self._test_doctype,
- 'xsl:stylesheet PUBLIC "htmlent.dtd"')
-
- def _test_doctype(self, doctype_fragment):
- """Run a battery of assertions on a given doctype string.
-
- HTMLParser doesn't actually behave like this, so this method
- is never called in this class. But many other builders do
- behave like this, so I've put the method in the superclass.
- """
- doctype_str = '<!DOCTYPE %s>' % doctype_fragment
- markup = doctype_str + '<p>foo</p>'
- soup = self.soup(markup)
- doctype = soup.contents[0]
- self.assertEqual(doctype.__class__, Doctype)
- self.assertEqual(doctype, doctype_fragment)
- self.assertEqual(str(soup)[:len(doctype_str)], doctype_str)
-
- # Make sure that the doctype was correctly associated with the
- # parse tree and that the rest of the document parsed.
- self.assertEqual(soup.p.contents[0], 'foo')
-
-# -------------------------
-
- def test_mixed_case_tags(self):
- # Mixed-case tags are folded to lowercase.
- self.assertSoupEquals(
- "<a><B><Cd><EFG></efg></CD></b></A>",
- "<a><b><cd><efg></efg></cd></b></a>")
-
-
- def test_empty_tag_thats_not_an_empty_element_tag(self):
- # A tag that is empty but not an HTML empty-element tag
- # is not presented as an empty-element tag.
- self.assertSoupEquals("<p>", "<p></p>")
-
- def test_comment(self):
- # Comments are represented as Comment objects.
- markup = "<p>foo<!--foobar-->baz</p>"
- self.assertSoupEquals(markup)
-
- soup = self.soup(markup)
- comment = soup.find(text="foobar")
- self.assertEqual(comment.__class__, Comment)
-
- def test_nested_inline_elements(self):
- # Inline tags can be nested indefinitely.
- b_tag = "<b>Inside a B tag</b>"
- self.assertSoupEquals(b_tag)
-
- nested_b_tag = "<p>A <i>nested <b>tag</b></i></p>"
- self.assertSoupEquals(nested_b_tag)
-
- double_nested_b_tag = "<p>A <a>doubly <i>nested <b>tag</b></i></a></p>"
- self.assertSoupEquals(nested_b_tag)
-
- def test_nested_block_level_elements(self):
- soup = self.soup('<blockquote><p><b>Foo</b></p></blockquote>')
- blockquote = soup.blockquote
- self.assertEqual(blockquote.p.b.string, 'Foo')
- self.assertEqual(blockquote.b.string, 'Foo')
-
- # This is a <table> tag containing another <table> tag in one of its
- # cells.
- TABLE_MARKUP_1 = ('<table id="1">'
- '<tr>'
- "<td>Here's another table:"
- '<table id="2">'
- '<tr><td>foo</td></tr>'
- '</table></td>')
-
- def test_correctly_nested_tables(self):
- markup = ('<table id="1">'
- '<tr>'
- "<td>Here's another table:"
- '<table id="2">'
- '<tr><td>foo</td></tr>'
- '</table></td>')
-
- self.assertSoupEquals(
- markup,
- '<table id="1"><tr><td>Here\'s another table:'
- '<table id="2"><tr><td>foo</td></tr></table>'
- '</td></tr></table>')
-
- self.assertSoupEquals(
- "<table><thead><tr><td>Foo</td></tr></thead>"
- "<tbody><tr><td>Bar</td></tr></tbody>"
- "<tfoot><tr><td>Baz</td></tr></tfoot></table>")
-
- def test_collapsed_whitespace(self):
- """In most tags, whitespace is collapsed."""
- self.assertSoupEquals("<p> </p>", "<p> </p>")
-
- def test_preserved_whitespace_in_pre_and_textarea(self):
- """In <pre> and <textarea> tags, whitespace is preserved."""
- self.assertSoupEquals("<pre> </pre>")
- self.assertSoupEquals("<textarea> woo </textarea>")
-
- def test_single_quote_attribute_values_become_double_quotes(self):
- self.assertSoupEquals("<foo attr='bar'></foo>",
- '<foo attr="bar"></foo>')
-
- def test_attribute_values_with_nested_quotes_are_left_alone(self):
- text = """<foo attr='bar "brawls" happen'>a</foo>"""
- self.assertSoupEquals(text)
-
- def test_attribute_values_with_double_nested_quotes_get_quoted(self):
- text = """<foo attr='bar "brawls" happen'>a</foo>"""
- soup = self.soup(text)
- soup.foo['attr'] = 'Brawls happen at "Bob\'s Bar"'
- self.assertSoupEquals(
- soup.foo.decode(),
- """<foo attr="Brawls happen at &quot;Bob\'s Bar&quot;">a</foo>""")
-
- def test_ampersand_in_attribute_value_gets_quoted(self):
- self.assertSoupEquals('<this is="really messed up & stuff"></this>',
- '<this is="really messed up &amp; stuff"></this>')
-
- def test_entities_in_strings_converted_during_parsing(self):
- # Both XML and HTML entities are converted to Unicode characters
- # during parsing.
- text = "<p>&lt;&lt;sacr&eacute;&#32;bleu!&gt;&gt;</p>"
- expected = u"<p>&lt;&lt;sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!&gt;&gt;</p>"
- self.assertSoupEquals(text, expected)
-
- def test_smart_quotes_converted_on_the_way_in(self):
- # Microsoft smart quotes are converted to Unicode characters during
- # parsing.
- quote = b"<p>\x91Foo\x92</p>"
- soup = self.soup(quote)
- self.assertEqual(
- soup.p.string,
- u"\N{LEFT SINGLE QUOTATION MARK}Foo\N{RIGHT SINGLE QUOTATION MARK}")
-
- def test_non_breaking_spaces_converted_on_the_way_in(self):
- soup = self.soup("<a>&nbsp;&nbsp;</a>")
- self.assertEqual(soup.a.string, u"\N{NO-BREAK SPACE}" * 2)
-
- def test_real_iso_latin_document(self):
- # Smoke test of interrelated functionality, using an
- # easy-to-understand document.
-
- # Here it is in Unicode. Note that it claims to be in ISO-Latin-1.
- unicode_html = u'<html><head><meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type"/></head><body><p>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</p></body></html>'
-
- # That's because we're going to encode it into ISO-Latin-1, and use
- # that to test.
- iso_latin_html = unicode_html.encode("iso-8859-1")
-
- # Parse the ISO-Latin-1 HTML.
- soup = self.soup(iso_latin_html)
- # Encode it to UTF-8.
- result = soup.encode("utf-8")
-
- # What do we expect the result to look like? Well, it would
- # look like unicode_html, except that the META tag would say
- # UTF-8 instead of ISO-Latin-1.
- expected = unicode_html.replace("ISO-Latin-1", "utf-8")
-
- # And, of course, it would be in UTF-8, not Unicode.
- expected = expected.encode("utf-8")
-
- # Ta-da!
- self.assertEqual(result, expected)
-
- def test_real_shift_jis_document(self):
- # Smoke test to make sure the parser can handle a document in
- # Shift-JIS encoding, without choking.
- shift_jis_html = (
- b'<html><head></head><body><pre>'
- b'\x82\xb1\x82\xea\x82\xcdShift-JIS\x82\xc5\x83R\x81[\x83f'
- b'\x83B\x83\x93\x83O\x82\xb3\x82\xea\x82\xbd\x93\xfa\x96{\x8c'
- b'\xea\x82\xcc\x83t\x83@\x83C\x83\x8b\x82\xc5\x82\xb7\x81B'
- b'</pre></body></html>')
- unicode_html = shift_jis_html.decode("shift-jis")
- soup = self.soup(unicode_html)
-
- # Make sure the parse tree is correctly encoded to various
- # encodings.
- self.assertEqual(soup.encode("utf-8"), unicode_html.encode("utf-8"))
- self.assertEqual(soup.encode("euc_jp"), unicode_html.encode("euc_jp"))
-
- # Tests below this line need work.
-
- def test_meta_tag_reflects_current_encoding(self):
- # Here's the <meta> tag saying that a document is
- # encoded in Shift-JIS.
- meta_tag = ('<meta content="text/html; charset=x-sjis" '
- 'http-equiv="Content-type"/>')
-
- # Here's a document incorporating that meta tag.
- shift_jis_html = (
- '<html><head>\n%s\n'
- '<meta http-equiv="Content-language" content="ja"/>'
- '</head><body>Shift-JIS markup goes here.') % meta_tag
- soup = self.soup(shift_jis_html)
-
- # Parse the document, and the charset is replaced with a
- # generic value.
- parsed_meta = soup.find('meta', {'http-equiv': 'Content-type'})
- self.assertEqual(parsed_meta['content'],
- 'text/html; charset=%SOUP-ENCODING%')
- self.assertEqual(parsed_meta.contains_substitutions, True)
-
- # For the rest of the story, see TestSubstitutions in
- # test_tree.py.
-
- def test_entities_converted_on_the_way_out(self):
- text = "<p>&lt;&lt;sacr&eacute;&#32;bleu!&gt;&gt;</p>"
- expected = u"&lt;&lt;sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!&gt;&gt;".encode("utf-8")
- soup = self.soup(text)
- str = soup.p.string
- #self.assertEqual(str.encode("utf-8"), expected)
-
- def test_br_tag_is_empty_element(self):
- """A <br> tag is designated as an empty-element tag."""
- soup = self.soup("<br></br>")
- self.assertTrue(soup.br.is_empty_element)
- self.assertEqual(str(soup.br), "<br/>")
-
- def test_p_tag_is_not_empty_element(self):
- """A <p> tag is not designated as an empty-element tag."""
- soup = self.soup("<p/>")
- self.assertFalse(soup.p.is_empty_element)
- self.assertEqual(str(soup.p), "<p></p>")
-
- def test_soupstrainer(self):
- strainer = SoupStrainer("b")
- soup = self.soup("A <b>bold</b> <meta/> <i>statement</i>",
- parse_only=strainer)
- self.assertEqual(soup.decode(), "<b>bold</b>")
-
- def test_deepcopy(self):
- # Make sure you can copy the builder. This is important because
- # the builder is part of a BeautifulSoup object, and we want to be
- # able to copy that.
- copy.deepcopy(self.default_builder)
-
-class TestHTMLParserTreeBuilderInvalidMarkup(SoupTest):
- """Tests of invalid markup for the default tree builder.
-
- Subclass this to test other builders.
-
- These are very likely to give different results for different tree
- builders. It's not required that a tree builder handle invalid
- markup at all.
- """
-
- @property
- def default_builder(self):
- return HTMLParserTreeBuilder()
-
- def test_table_containing_bare_markup(self):
- # Markup should be in table cells, not directly in the table.
- self.assertSoupEquals("<table><div>Foo</div></table>")
-
- def test_incorrectly_nested_table(self):
- # The second <table> tag is floating in the <tr> tag
- # rather than being inside a <td>.
- bad_markup = ('<table id="1">'
- '<tr>'
- "<td>Here's another table:</td>"
- '<table id="2">'
- '<tr><td>foo</td></tr>'
- '</table></td>')
-
-
- def test_unclosed_a_tag(self):
- # <a> tags really ought to be closed at some point.
- #
- # We have all the <div> tags because HTML5 says to duplicate
- # the <a> tag rather than closing it, and that's what html5lib
- # does.
- markup = """<div id="1">
- <a href="foo">
-</div>
-<div id="2">
- <div id="3">
- <a href="bar"></a>
- </div>
-</div>"""
-
- expect = """<div id="1">
-<a href="foo">
-</a></div>
-<div id="2">
-<div id="3">
-<a href="bar"></a>
-</div>
-</div>"""
- self.assertSoupEquals(markup, expect)
-
- def test_unclosed_block_level_elements(self):
- # Unclosed block-level elements should be closed.
- self.assertSoupEquals(
- '<blockquote><p><b>Foo</blockquote><p>Bar',
- '<blockquote><p><b>Foo</b></p></blockquote><p>Bar</p>')
-
- def test_fake_self_closing_tag(self):
- # If a self-closing tag presents as a normal tag, it's treated
- # as one.
- self.assertSoupEquals(
- "<item><link>http://foo.com/</link></item>",
- "<item><link>http://foo.com/</link></item>")
-
- def test_boolean_attribute_with_no_value(self):
- soup = self.soup("<table><td nowrap>foo</td></table>")
- self.assertEqual(soup.table.td['nowrap'], None)
-
- def test_incorrectly_nested_tables(self):
- self.assertSoupEquals(
- '<table><tr><table><tr id="nested">',
- '<table><tr><table><tr id="nested"></tr></table></tr></table>')
-
- def test_floating_text_in_table(self):
- self.assertSoupEquals("<table><td></td>foo<td>bar</td></table>")
-
- def test_paragraphs_containing_block_display_elements(self):
- markup = self.soup("<p>this is the definition:"
- "<dl><dt>first case</dt>")
- # The <p> tag is not closed before the <dl> tag begins.
- self.assertEqual(len(markup.p.contents), 2)
-
- def test_empty_element_tag_with_contents(self):
- self.assertSoupEquals("<br>foo</br>", "<br>foo</br>")
-
- def test_doctype_in_body(self):
- markup = "<p>one<!DOCTYPE foobar>two</p>"
- self.assertSoupEquals(markup)
-
- def test_nonsensical_declaration(self):
- # Declarations that don't make any sense are ignored.
- self.assertRaises(HTMLParseError, self.soup, '<! Foo = -8><p>a</p>')
-
- def test_whitespace_in_doctype(self):
- # A declaration that has extra whitespace is ignored.
- self.assertRaises(
- HTMLParseError, self.soup,
- '<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">' +
- '<p>foo</p>')
-
- def test_incomplete_declaration(self):
- self.assertRaises(HTMLParseError, self.soup, 'a<!b <p>c')
-
- def test_cdata_where_it_doesnt_belong(self):
- #CDATA sections are ignored.
- markup = "<div><![CDATA[foo]]>"
- soup = self.soup(markup)
- self.assertEquals(soup.div.contents[0], CData("foo"))
-
- def test_attribute_value_never_got_closed(self):
- markup = '<a href="http://foo.com/</a> and blah and blah'
- soup = self.soup(markup)
- self.assertEqual(soup.encode(), b"")
-
- def test_attribute_value_with_embedded_brackets(self):
- soup = self.soup('<a b="<a>">')
- self.assertEqual(soup.a['b'], '<a>')
-
- def test_nonexistent_entity(self):
- soup = self.soup("<p>foo&#bar;baz</p>")
- # This is very strange.
- self.assertEqual(soup.p.string, "foo<p")
-
- # Compare a real entity.
- soup = self.soup("<p>foo&#100;baz</p>")
- self.assertEqual(soup.p.string, "foodbaz")
-
- # Also compare html5lib, which preserves the &# before the
- # entity name.
-
- def test_entity_out_of_range(self):
- # An entity that's out of range will be replaced with
- # REPLACEMENT CHARACTER.
- soup = self.soup("<p>&#10000000000000;</p>")
- self.assertEqual(soup.p.string, u"\N{REPLACEMENT CHARACTER}")
-
- soup = self.soup("<p>&#x1000000000000;</p>")
- self.assertEqual(soup.p.string, u"\N{REPLACEMENT CHARACTER}")
-
- soup = self.soup("<p>&#1000000000;</p>")
- self.assertEqual(soup.p.string, u"\N{REPLACEMENT CHARACTER}")
-
-
- def test_entity_was_not_finished(self):
- soup = self.soup("<p>&lt;Hello&gt")
- # Compare html5lib, which completes the entity.
- self.assertEqual(soup.p.string, "<Hello")
-
- def test_document_ends_with_incomplete_declaration(self):
- soup = self.soup('<p>a<!b')
- # This becomes a string 'a'. The incomplete declaration is ignored.
- # Compare html5lib, which turns it into a comment.
- self.assertEqual(soup.p.contents, ['a'])
-
- def test_document_starts_with_bogus_declaration(self):
- self.assertRaises(HTMLParseError, self.soup, '<! Foo ><p>a</p>')
-
- def test_tag_name_contains_unicode(self):
- # Unicode characters in tag names are stripped.
- tag_name = u"<our\N{SNOWMAN}>Joe</our\N{SNOWMAN}>"
- self.assertSoupEquals("<our>Joe</our>")
-
- def test_multiple_values_for_the_same_attribute(self):
- markup = '<b b="20" a="1" b="10" a="2" a="3" a="4"></b>'
- self.assertSoupEquals(markup, '<b a="4" b="10"></b>')
-
-class TestHTMLParserTreeBuilderEncodingConversion(SoupTest):
- # Test Beautiful Soup's ability to decode and encode from various
- # encodings.
-
- @property
- def default_builder(self):
- return HTMLParserTreeBuilder()
-
- def setUp(self):
- super(TestHTMLParserTreeBuilderEncodingConversion, self).setUp()
- self.unicode_data = u"<html><head></head><body><foo>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</foo></body></html>"
- self.utf8_data = self.unicode_data.encode("utf-8")
- # Just so you know what it looks like.
- self.assertEqual(
- self.utf8_data,
- b"<html><head></head><body><foo>Sacr\xc3\xa9 bleu!</foo></body></html>")
-
- def test_ascii_in_unicode_out(self):
- # ASCII input is converted to Unicode. The original_encoding
- # attribute is set.
- ascii = b"<foo>a</foo>"
- soup_from_ascii = self.soup(ascii)
- unicode_output = soup_from_ascii.decode()
- self.assertTrue(isinstance(unicode_output, unicode))
- self.assertEqual(unicode_output, self.document_for(ascii.decode()))
- self.assertEqual(soup_from_ascii.original_encoding, "ascii")
-
- def test_unicode_in_unicode_out(self):
- # Unicode input is left alone. The original_encoding attribute
- # is not set.
- soup_from_unicode = self.soup(self.unicode_data)
- self.assertEqual(soup_from_unicode.decode(), self.unicode_data)
- self.assertEqual(soup_from_unicode.foo.string, u'Sacr\xe9 bleu!')
- self.assertEqual(soup_from_unicode.original_encoding, None)
-
- def test_utf8_in_unicode_out(self):
- # UTF-8 input is converted to Unicode. The original_encoding
- # attribute is set.
- soup_from_utf8 = self.soup(self.utf8_data)
- self.assertEqual(soup_from_utf8.decode(), self.unicode_data)
- self.assertEqual(soup_from_utf8.foo.string, u'Sacr\xe9 bleu!')
-
- def test_utf8_out(self):
- # The internal data structures can be encoded as UTF-8.
- soup_from_unicode = self.soup(self.unicode_data)
- self.assertEqual(soup_from_unicode.encode('utf-8'), self.utf8_data)
-
- HEBREW_DOCUMENT = b'<html><head><title>Hebrew (ISO 8859-8) in Visual Directionality</title></head><body><h1>Hebrew (ISO 8859-8) in Visual Directionality</h1>\xed\xe5\xec\xf9</body></html>'
-
- def test_real_hebrew_document(self):
- # A real-world test to make sure we can convert ISO-8859-9 (a
- # Hebrew encoding) to UTF-8.
- soup = self.soup(self.HEBREW_DOCUMENT,
- from_encoding="iso-8859-8")
- self.assertEqual(soup.original_encoding, 'iso-8859-8')
- self.assertEqual(
- soup.encode('utf-8'),
- self.HEBREW_DOCUMENT.decode("iso-8859-8").encode("utf-8"))
+ # html.parser can't handle namespaced doctypes, so skip this one.
+ pass
diff --git a/bs4/tests/test_lxml.py b/bs4/tests/test_lxml.py
index 4d19e7f..92b7389 100644
--- a/bs4/tests/test_lxml.py
+++ b/bs4/tests/test_lxml.py
@@ -12,238 +12,26 @@ from bs4 import BeautifulSoup
from bs4.element import Comment, Doctype, SoupStrainer
from bs4.testing import skipIf
from bs4.tests import test_htmlparser
-from bs4.testing import skipIf
+from bs4.testing import (
+ HTMLTreeBuilderSmokeTest,
+ SoupTest,
+ skipIf,
+)
@skipIf(
not LXML_PRESENT,
"lxml seems not to be present, not testing its tree builder.")
-class TestLXMLTreeBuilder(test_htmlparser.TestHTMLParserTreeBuilder):
- """A smoke test for the LXML tree builder.
-
- Subclass this to test some other HTML tree builder. Subclasses of
- this test ensure that all of Beautiful Soup's tree builders
- generate more or less the same trees.
-
- It's okay for trees to differ--just override the appropriate test
- method to demonstrate how one tree builder differs from the LXML
- builder. But in general, all HTML tree builders should generate
- trees that make most of these tests pass.
- """
+class LXMLTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest):
+ """See ``HTMLTreeBuilderSmokeTest``."""
@property
def default_builder(self):
return LXMLTreeBuilder()
- def test_bare_string(self):
- # A bare string is turned into some kind of HTML document or
- # fragment recognizable as the original string.
- #
- # In this case, lxml puts a <p> tag around the bare string.
- self.assertSoupEquals(
- "A bare string", "<p>A bare string</p>")
-
- def test_cdata_where_its_ok(self):
- # lxml strips CDATA sections, no matter where they occur.
- markup = "<svg><![CDATA[foobar]]>"
- self.assertSoupEquals(markup, "<svg></svg>")
-
- def test_empty_element(self):
- # HTML's empty-element tags are recognized as such.
- self.assertSoupEquals(
- "<p>A <meta> tag</p>", "<p>A <meta/> tag</p>")
-
+ def test_out_of_range_entity(self):
self.assertSoupEquals(
- "<p>Foo<br/>bar</p>", "<p>Foo<br/>bar</p>")
-
- def test_naked_ampersands(self):
- # Ampersands are left alone.
- text = "<p>AT&T</p>"
- soup = self.soup(text)
- self.assertEqual(soup.p.string, "AT&T")
-
- # Even if they're in attribute values.
- invalid_url = '<a href="http://example.org?a=1&b=2;3">foo</a>'
- soup = self.soup(invalid_url)
- self.assertEqual(soup.a['href'], "http://example.org?a=1&b=2;3")
-
- def test_literal_in_textarea(self):
- # Anything inside a <textarea> is supposed to be treated as
- # the literal value of the field, (XXX citation
- # needed). html5lib does this correctly. But, lxml does its
- # best to parse the contents of a <textarea> as HTML.
- text = '<textarea>Junk like <b> tags and <&<&amp;</textarea>'
- soup = self.soup(text)
- self.assertEqual(len(soup.textarea.contents), 2)
- self.assertEqual(soup.textarea.contents[0], u"Junk like ")
- self.assertEqual(soup.textarea.contents[1].name, 'b')
- self.assertEqual(soup.textarea.b.string, u" tags and ")
-
- def test_literal_in_script(self):
- # The contents of a <script> tag are treated as a literal string,
- # even if that string contains HTML.
- javascript = 'if (i < 2) { alert("<b>foo</b>"); }'
- soup = self.soup('<script>%s</script>' % javascript)
- self.assertEqual(soup.script.string, javascript)
-
- def test_doctype(self):
- # Test a normal HTML doctype you'll commonly see in a real document.
- self._test_doctype(
- 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"')
-
- def test_namespaced_system_doctype(self):
- # Test a namespaced doctype with a system id.
- self._test_doctype('xsl:stylesheet SYSTEM "htmlent.dtd"')
-
- def test_namespaced_public_doctype(self):
- # Test a namespaced doctype with a public id.
- self._test_doctype('xsl:stylesheet PUBLIC "htmlent.dtd"')
-
- def test_entities_in_attribute_values_converted_during_parsing(self):
-
- # The numeric entity isn't recognized without the closing
- # semicolon.
- text = '<x t="pi&#241ata">'
- expected = u"pi\N{LATIN SMALL LETTER N WITH TILDE}ata"
- soup = self.soup(text)
- self.assertEqual(soup.x['t'], expected)
-
- text = '<x t="pi&#241;ata">'
- expected = u"pi\N{LATIN SMALL LETTER N WITH TILDE}ata"
- soup = self.soup(text)
- self.assertEqual(soup.x['t'], u"pi\xf1ata")
-
- text = '<x t="pi&#xf1;ata">'
- soup = self.soup(text)
- self.assertEqual(soup.x['t'], expected)
-
- text = '<x t="sacr&eacute; bleu">'
- soup = self.soup(text)
- self.assertEqual(
- soup.x['t'],
- u"sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu")
-
- # This can cause valid HTML to become invalid.
- valid_url = '<a href="http://example.org?a=1&amp;b=2;3">foo</a>'
- soup = self.soup(valid_url)
- self.assertEqual(soup.a['href'], "http://example.org?a=1&b=2;3")
-
-
-@skipIf(
- not LXML_PRESENT,
- "lxml seems not to be present, not testing it on invalid markup.")
-class TestLXMLTreeBuilderInvalidMarkup(
- test_htmlparser.TestHTMLParserTreeBuilderInvalidMarkup):
-
- @property
- def default_builder(self):
- return LXMLTreeBuilder()
-
- def test_attribute_value_never_got_closed(self):
- markup = '<a href="http://foo.com/</a> and blah and blah'
- soup = self.soup(markup)
- self.assertEqual(
- soup.a['href'], "http://foo.com/</a> and blah and blah")
-
- def test_attribute_value_was_closed_by_subsequent_tag(self):
- markup = """<a href="foo</a>, </a><a href="bar">baz</a>"""
- soup = self.soup(markup)
- # The string between the first and second quotes was interpreted
- # as the value of the 'href' attribute.
- self.assertEqual(soup.a['href'], 'foo</a>, </a><a href=')
-
- #The string after the second quote (bar"), was treated as an
- #empty attribute called bar.
- self.assertEqual(soup.a['bar'], '')
- self.assertEqual(soup.a.string, "baz")
-
- def test_document_starts_with_bogus_declaration(self):
- soup = self.soup('<! Foo ><p>a</p>')
- # The declaration is ignored altogether.
- self.assertEqual(soup.encode(), b"<html><body><p>a</p></body></html>")
-
- def test_incomplete_declaration(self):
- # An incomplete declaration will screw up the rest of the document.
- self.assertSoupEquals('a<!b <p>c', '<p>a</p>')
-
- def test_nonsensical_declaration(self):
- # Declarations that don't make any sense are ignored.
- self.assertSoupEquals('<! Foo = -8><p>a</p>', "<p>a</p>")
-
- def test_unquoted_attribute_value(self):
- soup = self.soup('<a style={height:21px;}></a>')
- self.assertEqual(soup.a['style'], '{height:21px;}')
-
- def test_whitespace_in_doctype(self):
- # A declaration that has extra whitespace is ignored.
+ "<p>foo&#10000000000000;bar</p>", "<p>foobar</p>")
self.assertSoupEquals(
- ('<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">'
- '<p>foo</p>'),
- '<p>foo</p>')
-
- def test_boolean_attribute_with_no_value(self):
- soup = self.soup("<table><td nowrap>foo</td></table>")
- self.assertEqual(soup.table.td['nowrap'], '')
-
- def test_cdata_where_it_doesnt_belong(self):
- #CDATA sections are ignored.
- markup = "<div><![CDATA[foo]]>"
- self.assertSoupEquals(markup, "<div></div>")
-
- def test_empty_element_tag_with_contents(self):
- self.assertSoupEquals("<br>foo</br>", "<br/>foo")
-
- def test_nonexistent_entity(self):
- soup = self.soup("<p>foo&#bar;baz</p>")
- self.assertEqual(soup.p.string, "foobar;baz")
-
- # Compare a real entity.
- soup = self.soup("<p>foo&#100;baz</p>")
- self.assertEqual(soup.p.string, "foodbaz")
-
- # Also compare html5lib, which preserves the &# before the
- # entity name.
-
- def test_entity_was_not_finished(self):
- soup = self.soup("<p>&lt;Hello&gt")
- # Compare html5lib, which completes the entity.
- self.assertEqual(soup.p.string, "<Hello&gt")
-
- def test_fake_self_closing_tag(self):
- # If a self-closing tag presents as a normal tag, the 'open'
- # tag is treated as an instance of the self-closing tag and
- # the 'close' tag is ignored.
+ "<p>foo&#x10000000000000;bar</p>", "<p>foobar</p>")
self.assertSoupEquals(
- "<item><link>http://foo.com/</link></item>",
- "<item><link/>http://foo.com/</item>")
-
- def test_paragraphs_containing_block_display_elements(self):
- markup = self.soup("<p>this is the definition:"
- "<dl><dt>first case</dt>")
- # The <p> tag is closed before the <dl> tag begins.
- self.assertEqual(markup.p.contents, ["this is the definition:"])
-
- def test_multiple_values_for_the_same_attribute(self):
- markup = '<b b="20" a="1" b="10" a="2" a="3" a="4"></b>'
- self.assertSoupEquals(markup, '<b a="1" b="20"></b>')
-
- def test_entity_out_of_range(self):
- # An entity that's out of range will be ignored.
- soup = self.soup("<p>&#10000000000000;</p>")
- self.assertEqual(0, len(soup.p.contents))
-
- soup = self.soup("<p>&#x1000000000000;</p>")
- self.assertEqual(0, len(soup.p.contents))
-
- soup = self.soup("<p>&#1000000000;</p>")
- self.assertEqual(0, len(soup.p.contents))
-
-
-@skipIf(
- not LXML_PRESENT,
- "lxml seems not to be present, not testing it on encoding conversion.")
-class TestLXMLParserTreeBuilderEncodingConversion(
- test_htmlparser.TestHTMLParserTreeBuilderEncodingConversion):
-
- @property
- def default_builder(self):
- return LXMLTreeBuilder()
+ "<p>foo&#1000000000;bar</p>", "<p>foobar</p>")
diff --git a/bs4/tests/test_soup.py b/bs4/tests/test_soup.py
index 896e914..2b7c003 100644
--- a/bs4/tests/test_soup.py
+++ b/bs4/tests/test_soup.py
@@ -103,6 +103,51 @@ class TestEntitySubstitution(unittest.TestCase):
text = 'Bob\'s "bar"'
self.assertEqual(self.sub.substitute_html(text), text)
+
+class TestEncodingConversion(SoupTest):
+ # Test Beautiful Soup's ability to decode and encode from various
+ # encodings.
+
+ def setUp(self):
+ super(TestEncodingConversion, self).setUp()
+ self.unicode_data = u"<html><head></head><body><foo>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</foo></body></html>"
+ self.utf8_data = self.unicode_data.encode("utf-8")
+ # Just so you know what it looks like.
+ self.assertEqual(
+ self.utf8_data,
+ b"<html><head></head><body><foo>Sacr\xc3\xa9 bleu!</foo></body></html>")
+
+ def test_ascii_in_unicode_out(self):
+ # ASCII input is converted to Unicode. The original_encoding
+ # attribute is set.
+ ascii = b"<foo>a</foo>"
+ soup_from_ascii = self.soup(ascii)
+ unicode_output = soup_from_ascii.decode()
+ self.assertTrue(isinstance(unicode_output, unicode))
+ self.assertEqual(unicode_output, self.document_for(ascii.decode()))
+ self.assertEqual(soup_from_ascii.original_encoding, "ascii")
+
+ def test_unicode_in_unicode_out(self):
+ # Unicode input is left alone. The original_encoding attribute
+ # is not set.
+ soup_from_unicode = self.soup(self.unicode_data)
+ self.assertEqual(soup_from_unicode.decode(), self.unicode_data)
+ self.assertEqual(soup_from_unicode.foo.string, u'Sacr\xe9 bleu!')
+ self.assertEqual(soup_from_unicode.original_encoding, None)
+
+ def test_utf8_in_unicode_out(self):
+ # UTF-8 input is converted to Unicode. The original_encoding
+ # attribute is set.
+ soup_from_utf8 = self.soup(self.utf8_data)
+ self.assertEqual(soup_from_utf8.decode(), self.unicode_data)
+ self.assertEqual(soup_from_utf8.foo.string, u'Sacr\xe9 bleu!')
+
+ def test_utf8_out(self):
+ # The internal data structures can be encoded as UTF-8.
+ soup_from_unicode = self.soup(self.unicode_data)
+ self.assertEqual(soup_from_unicode.encode('utf-8'), self.utf8_data)
+
+
class TestUnicodeDammit(unittest.TestCase):
"""Standalone tests of Unicode, Dammit."""
diff --git a/scripts/demonstrate_parser_differences.py b/scripts/demonstrate_parser_differences.py
new file mode 100644
index 0000000..d84670a
--- /dev/null
+++ b/scripts/demonstrate_parser_differences.py
@@ -0,0 +1,95 @@
+"""Demonstrate how different parsers parse the same markup.
+
+Beautiful Soup can use any of a number of different parsers. Every
+parser should behave more or less the same on valid markup, and
+Beautiful Soup's unit tests make sure this is the case. But every
+parser handles invalid markup differently. Even different versions of
+the same parser handle invalid markup differently. So instead of unit
+tests I've created this educational demonstration script.
+
+The file demonstration_markup.txt contains many lines of HTML. This
+script tests each line of markup against every parser you have
+installed, and prints out how each parser sees that markup. This may
+help you choose a parser, or understand why Beautiful Soup presents
+your document the way it does.
+"""
+
+import os
+import sys
+from bs4 import BeautifulSoup
+parsers = ['html.parser']
+
+try:
+ from bs4.builder import _lxml
+ parsers.append('lxml')
+except ImportError, e:
+ pass
+
+try:
+ from bs4.builder import _html5lib
+ parsers.append('html5lib')
+except ImportError, e:
+ pass
+
+class Demonstration(object):
+ def __init__(self, markup):
+ self.results = {}
+ self.markup = markup
+
+ def run_against(self, *parser_names):
+ uniform_results = True
+ previous_output = None
+ for parser in parser_names:
+ try:
+ soup = BeautifulSoup(self.markup, parser)
+ if markup.startswith("<div>"):
+ # Extract the interesting part
+ output = soup.div
+ else:
+ output = soup
+ except Exception, e:
+ output = "[EXCEPTION] %s" % str(e)
+ self.results[parser] = output
+ if previous_output is None:
+ previous_output = output
+ elif previous_output != output:
+ uniform_results = False
+ return uniform_results
+
+ def dump(self):
+ print "%s: %s" % ("Markup".rjust(13), self.markup.encode("utf8"))
+ for parser, output in self.results.items():
+ print "%s: %s" % (parser.rjust(13), output.encode("utf8"))
+
+different_results = []
+uniform_results = []
+
+print "= Testing the following parsers: %s =" % ", ".join(parsers)
+print
+
+input_file = sys.stdin
+if sys.stdin.isatty():
+ for filename in [
+ "demonstration_markup.txt",
+ os.path.join("scripts", "demonstration_markup.txt")]:
+ if os.path.exists(filename):
+ input_file = open(filename)
+
+for markup in input_file:
+ demo = Demonstration(markup.decode("utf8").strip().replace("\\n", "\n"))
+ is_uniform = demo.run_against(*parsers)
+ if is_uniform:
+ uniform_results.append(demo)
+ else:
+ different_results.append(demo)
+
+print "== Markup that's handled the same in every parser =="
+print
+for demo in uniform_results:
+ demo.dump()
+ print
+print "== Markup that's not handled the same in every parser =="
+print
+for demo in different_results:
+ demo.dump()
+ print
diff --git a/scripts/demonstration_markup.txt b/scripts/demonstration_markup.txt
new file mode 100644
index 0000000..a7914a0
--- /dev/null
+++ b/scripts/demonstration_markup.txt
@@ -0,0 +1,34 @@
+A bare string
+<!DOCTYPE xsl:stylesheet SYSTEM "htmlent.dtd">
+<!DOCTYPE xsl:stylesheet PUBLIC "htmlent.dtd">
+<div><![CDATA[A CDATA section where it doesn't belong]]></div>
+<div><svg><![CDATA[HTML5 does allow CDATA sections in SVG]]></svg></div>
+<div>A <meta> tag</div>
+<div>A <br> tag that supposedly has contents.</br></div>
+<div>AT&T</div>
+<div><textarea>Within a textarea, markup like <b> tags and <&<&amp; should be treated as literal</textarea></div>
+<div><script>if (i < 2) { alert("<b>Markup within script tags should be treated as literal.</b>"); }</script></div>
+<div>This numeric entity is missing the final semicolon: <x t="pi&#241ata"></div>
+<div><a href="http://example.com/</a> that attribute value never got closed</div>
+<div><a href="foo</a>, </a><a href="bar">that attribute value was closed by the subsequent tag</a></div>
+<! This document starts with a bogus declaration ><div>a</div>
+<div>This document contains <!an incomplete declaration <div>(do you see it?)</div>
+<div>This document ends with <!an incomplete declaration
+<div><a style={height:21px;}>That attribute value was bogus</a></div>
+<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">The doctype is invalid because it contains extra whitespace
+<div><table><td nowrap>That boolean attribute had no value</td></table></div>
+<div>Here's a nonexistent entity: &#foo; (do you see it?)</div>
+<div>This document ends before the entity finishes: &gt
+<div><p>Paragraphs shouldn't contain block display elements, but this one does: <dl><dt>you see?</dt></p>
+<b b="20" a="1" b="10" a="2" a="3" a="4">Multiple values for the same attribute.</b>
+<div><table><tr><td>Here's a table</td></tr></table></div>
+<div><table id="1"><tr><td>Here's a nested table:<table id="2"><tr><td>foo</td></tr></table></td></div>
+<div>This tag contains nothing but whitespace: <b> </b></div>
+<div><blockquote><p><b>This p tag is cut off by</blockquote></p>the end of the blockquote tag</div>
+<div><table><div>This table contains bare markup</div></table></div>
+<div><div id="1">\n <a href="link1">This link is never closed.\n</div>\n<div id="2">\n <div id="3">\n <a href="link2">This link is closed.</a>\n </div>\n</div></div>
+<div>This document contains a <!DOCTYPE surprise>surprise doctype</div>
+<div><a><B><Cd><EFG>Mixed case tags are folded to lowercase</efg></CD></b></A></div>
+<div><our☃>Tag name contains Unicode characters</our☃></div>
+<div><a ☃="snowman">Attribute name contains Unicode characters</a></div>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">