@@ -0,0 +1,830 @@
|
||||
"""
|
||||
Unit tests for EPUB reader functionality.
|
||||
|
||||
Tests the EPUB parsing and conversion to pyWebLayout abstract elements,
|
||||
using ebooklib to generate test EPUB files.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
# Import ebooklib for creating test EPUB files
|
||||
try:
|
||||
from ebooklib import epub
|
||||
EBOOKLIB_AVAILABLE = True
|
||||
except ImportError:
|
||||
EBOOKLIB_AVAILABLE = False
|
||||
|
||||
from pyWebLayout.io.readers.epub_reader import read_epub, EPUBReader
|
||||
from pyWebLayout.abstract.document import Book
|
||||
from pyWebLayout.abstract.block import (
|
||||
Paragraph, Heading, HeadingLevel, Quote, CodeBlock,
|
||||
HList, ListStyle, Table, HorizontalRule, Image
|
||||
)
|
||||
from pyWebLayout.style import FontWeight, FontStyle, TextDecoration
|
||||
|
||||
|
||||
@unittest.skipUnless(EBOOKLIB_AVAILABLE, "ebooklib not available")
|
||||
class TestEPUBReader(unittest.TestCase):
|
||||
"""Test cases for EPUB reader functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.epub_files = []
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
# Clean up test EPUB files
|
||||
for epub_file in self.epub_files:
|
||||
try:
|
||||
os.remove(epub_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Clean up test directory
|
||||
if os.path.exists(self.test_dir):
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
|
||||
def create_simple_epub(self, title="Test Book", author="Test Author"):
|
||||
"""Create a simple EPUB file for testing."""
|
||||
book = epub.EpubBook()
|
||||
|
||||
# Set metadata
|
||||
book.set_identifier('test-id-123')
|
||||
book.set_title(title)
|
||||
book.set_language('en')
|
||||
book.add_author(author)
|
||||
|
||||
# Create a simple chapter
|
||||
chapter1 = epub.EpubHtml(
|
||||
title='Chapter 1',
|
||||
file_name='chapter1.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter1.content = '''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Chapter 1</title></head>
|
||||
<body>
|
||||
<h1>Chapter One</h1>
|
||||
<p>This is the first paragraph of the first chapter.</p>
|
||||
<p>This is a <strong>second paragraph</strong> with <em>some formatting</em>.</p>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# Add chapter to book
|
||||
book.add_item(chapter1)
|
||||
|
||||
# Define table of contents
|
||||
book.toc = (epub.Link("chapter1.xhtml", "Chapter 1", "ch1"),)
|
||||
|
||||
# Add navigation files
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
|
||||
# Define spine
|
||||
book.spine = ['nav', chapter1]
|
||||
|
||||
# Create temporary file
|
||||
epub_path = os.path.join(self.test_dir, f'test_simple_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
return epub_path
|
||||
|
||||
def create_complex_epub(self):
|
||||
"""Create a more complex EPUB file with multiple chapters and content types."""
|
||||
book = epub.EpubBook()
|
||||
|
||||
# Set metadata
|
||||
book.set_identifier('complex-test-id-456')
|
||||
book.set_title('Complex Test Book')
|
||||
book.set_language('en')
|
||||
book.add_author('Test Author')
|
||||
book.add_metadata('DC', 'description', 'A test book with complex content')
|
||||
book.add_metadata('DC', 'subject', 'Testing')
|
||||
book.add_metadata('DC', 'date', '2024-01-01')
|
||||
book.add_metadata('DC', 'publisher', 'Test Publisher')
|
||||
|
||||
# Chapter 1: Basic content
|
||||
chapter1 = epub.EpubHtml(
|
||||
title='Introduction',
|
||||
file_name='chapter1.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter1.content = '''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Introduction</title></head>
|
||||
<body>
|
||||
<h1>Introduction</h1>
|
||||
<p>Welcome to this <strong>complex test book</strong>.</p>
|
||||
<p>This chapter contains basic content to test paragraph parsing.</p>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# Chapter 2: Styled content
|
||||
chapter2 = epub.EpubHtml(
|
||||
title='Styled Content',
|
||||
file_name='chapter2.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter2.content = '''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Styled Content</title></head>
|
||||
<body>
|
||||
<h1>Styled Content</h1>
|
||||
<p>This chapter contains various <strong>bold text</strong>, <em>italic text</em>,
|
||||
and <span style="color: red; font-weight: bold;">colored text</span>.</p>
|
||||
<h2>Subsection</h2>
|
||||
<p>Text with <u>underline</u> and <s>strikethrough</s>.</p>
|
||||
<h3>More Formatting</h3>
|
||||
<p>Nested formatting: <strong>bold with <em>italic inside</em></strong>.</p>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# Chapter 3: Lists and quotes
|
||||
chapter3 = epub.EpubHtml(
|
||||
title='Lists and Quotes',
|
||||
file_name='chapter3.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter3.content = '''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Lists and Quotes</title></head>
|
||||
<body>
|
||||
<h1>Lists and Quotes</h1>
|
||||
|
||||
<h2>Unordered List</h2>
|
||||
<ul>
|
||||
<li>First item</li>
|
||||
<li><strong>Bold item</strong></li>
|
||||
<li>Item with <em>italic text</em></li>
|
||||
</ul>
|
||||
|
||||
<h2>Ordered List</h2>
|
||||
<ol>
|
||||
<li>First numbered item</li>
|
||||
<li>Second numbered item</li>
|
||||
<li>Third numbered item</li>
|
||||
</ol>
|
||||
|
||||
<h2>Quote</h2>
|
||||
<blockquote>
|
||||
<p>This is a <span style="font-style: italic;">quoted paragraph</span>
|
||||
with some styling.</p>
|
||||
</blockquote>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# Chapter 4: Tables and code
|
||||
chapter4 = epub.EpubHtml(
|
||||
title='Tables and Code',
|
||||
file_name='chapter4.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter4.content = '''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Tables and Code</title></head>
|
||||
<body>
|
||||
<h1>Tables and Code</h1>
|
||||
|
||||
<h2>Simple Table</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Header 1</strong></th>
|
||||
<th><em>Header 2</em></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Cell 1</td>
|
||||
<td>Cell 2 with <span style="color: blue;">blue text</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Bold cell</strong></td>
|
||||
<td>Normal cell</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Code Block</h2>
|
||||
<pre><code>function test() {
|
||||
console.log("Hello, world!");
|
||||
return true;
|
||||
}</code></pre>
|
||||
|
||||
<h2>Inline Code</h2>
|
||||
<p>Use the <code>print()</code> function to output text.</p>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# Add chapters to book
|
||||
book.add_item(chapter1)
|
||||
book.add_item(chapter2)
|
||||
book.add_item(chapter3)
|
||||
book.add_item(chapter4)
|
||||
|
||||
# Define table of contents
|
||||
book.toc = (
|
||||
epub.Link("chapter1.xhtml", "Introduction", "intro"),
|
||||
epub.Link("chapter2.xhtml", "Styled Content", "styled"),
|
||||
epub.Link("chapter3.xhtml", "Lists and Quotes", "lists"),
|
||||
epub.Link("chapter4.xhtml", "Tables and Code", "tables")
|
||||
)
|
||||
|
||||
# Add navigation files
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
|
||||
# Define spine
|
||||
book.spine = ['nav', chapter1, chapter2, chapter3, chapter4]
|
||||
|
||||
# Create temporary file
|
||||
epub_path = os.path.join(self.test_dir, f'test_complex_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
return epub_path
|
||||
|
||||
def create_epub_with_nested_content(self):
|
||||
"""Create an EPUB with nested content structures."""
|
||||
book = epub.EpubBook()
|
||||
|
||||
# Set metadata
|
||||
book.set_identifier('nested-test-id-789')
|
||||
book.set_title('Nested Content Test')
|
||||
book.set_language('en')
|
||||
book.add_author('Test Author')
|
||||
|
||||
# Chapter with nested content
|
||||
chapter = epub.EpubHtml(
|
||||
title='Nested Content',
|
||||
file_name='nested.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter.content = '''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Nested Content</title></head>
|
||||
<body>
|
||||
<h1>Nested Content Examples</h1>
|
||||
|
||||
<div>
|
||||
<h2>Section in Div</h2>
|
||||
<p>Paragraph inside div.</p>
|
||||
|
||||
<section>
|
||||
<h3>Subsection</h3>
|
||||
<article>
|
||||
<h4>Article Header</h4>
|
||||
<p>Article content with <strong>nested <em>formatting</em></strong>.</p>
|
||||
|
||||
<aside>
|
||||
<p>Sidebar content in aside element.</p>
|
||||
<ul>
|
||||
<li>Nested list item</li>
|
||||
<li>Another <strong>bold</strong> item</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Footer content with <span style="font-size: 12px; color: gray;">small gray text</span>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# Add chapter to book
|
||||
book.add_item(chapter)
|
||||
|
||||
# Define table of contents
|
||||
book.toc = (epub.Link("nested.xhtml", "Nested Content", "nested"),)
|
||||
|
||||
# Add navigation files
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
|
||||
# Define spine
|
||||
book.spine = ['nav', chapter]
|
||||
|
||||
# Create temporary file
|
||||
epub_path = os.path.join(self.test_dir, f'test_nested_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
return epub_path
|
||||
|
||||
def test_simple_epub_reading(self):
|
||||
"""Test reading a simple EPUB file."""
|
||||
epub_path = self.create_simple_epub()
|
||||
|
||||
# Read the EPUB
|
||||
book = read_epub(epub_path)
|
||||
|
||||
# Verify it's a Book object
|
||||
self.assertIsInstance(book, Book)
|
||||
|
||||
# Check metadata
|
||||
self.assertEqual(book.title, "Test Book")
|
||||
|
||||
# Check chapters
|
||||
chapters = list(book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
# Check chapter content
|
||||
chapter = chapters[0]
|
||||
blocks = list(chapter.blocks)
|
||||
self.assertGreater(len(blocks), 0)
|
||||
|
||||
# Should have a heading and paragraphs
|
||||
has_heading = any(isinstance(block, Heading) for block in blocks)
|
||||
has_paragraph = any(isinstance(block, Paragraph) for block in blocks)
|
||||
|
||||
self.assertTrue(has_heading, "Should contain at least one heading")
|
||||
self.assertTrue(has_paragraph, "Should contain at least one paragraph")
|
||||
|
||||
def test_complex_epub_reading(self):
|
||||
"""Test reading a complex EPUB file with multiple chapters."""
|
||||
epub_path = self.create_complex_epub()
|
||||
|
||||
# Read the EPUB
|
||||
book = read_epub(epub_path)
|
||||
|
||||
# Verify it's a Book object
|
||||
self.assertIsInstance(book, Book)
|
||||
|
||||
# Check metadata
|
||||
self.assertEqual(book.title, "Complex Test Book")
|
||||
|
||||
# Check chapters
|
||||
chapters = list(book.chapters)
|
||||
self.assertEqual(len(chapters), 4)
|
||||
|
||||
# Test each chapter has content
|
||||
for i, chapter in enumerate(chapters):
|
||||
blocks = list(chapter.blocks)
|
||||
self.assertGreater(len(blocks), 0, f"Chapter {i+1} should have blocks")
|
||||
|
||||
# Each chapter should start with a heading
|
||||
first_block = blocks[0]
|
||||
self.assertIsInstance(first_block, Heading, f"Chapter {i+1} should start with heading")
|
||||
|
||||
def test_epub_styled_content(self):
|
||||
"""Test that styled content in EPUB is properly parsed."""
|
||||
epub_path = self.create_complex_epub()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
chapters = list(book.chapters)
|
||||
|
||||
# Check styled content in chapter 2 (index 1)
|
||||
if len(chapters) > 1:
|
||||
chapter2_blocks = list(chapters[1].blocks)
|
||||
|
||||
# Find paragraphs with styled text
|
||||
styled_words_found = False
|
||||
for block in chapter2_blocks:
|
||||
if isinstance(block, Paragraph):
|
||||
words = list(block.words())
|
||||
for _, word in words:
|
||||
if (word.style.weight == FontWeight.BOLD or
|
||||
word.style.style == FontStyle.ITALIC or
|
||||
word.style.colour != (0, 0, 0)): # Non-black color
|
||||
styled_words_found = True
|
||||
break
|
||||
if styled_words_found:
|
||||
break
|
||||
|
||||
self.assertTrue(styled_words_found, "Should find styled words in chapter 2")
|
||||
|
||||
def test_epub_lists(self):
|
||||
"""Test that lists in EPUB are properly parsed."""
|
||||
epub_path = self.create_complex_epub()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
chapters = list(book.chapters)
|
||||
|
||||
# Check lists in chapter 3 (index 2)
|
||||
if len(chapters) > 2:
|
||||
chapter3_blocks = list(chapters[2].blocks)
|
||||
|
||||
# Find list blocks
|
||||
unordered_list_found = False
|
||||
ordered_list_found = False
|
||||
quote_found = False
|
||||
|
||||
for block in chapter3_blocks:
|
||||
if isinstance(block, HList):
|
||||
if block.style == ListStyle.UNORDERED:
|
||||
unordered_list_found = True
|
||||
|
||||
# Check list items
|
||||
items = list(block.items())
|
||||
self.assertGreater(len(items), 0, "Unordered list should have items")
|
||||
|
||||
elif block.style == ListStyle.ORDERED:
|
||||
ordered_list_found = True
|
||||
|
||||
# Check list items
|
||||
items = list(block.items())
|
||||
self.assertGreater(len(items), 0, "Ordered list should have items")
|
||||
|
||||
elif isinstance(block, Quote):
|
||||
quote_found = True
|
||||
|
||||
self.assertTrue(unordered_list_found, "Should find unordered list in chapter 3")
|
||||
self.assertTrue(ordered_list_found, "Should find ordered list in chapter 3")
|
||||
self.assertTrue(quote_found, "Should find quote in chapter 3")
|
||||
|
||||
def test_epub_tables(self):
|
||||
"""Test that tables in EPUB are properly parsed."""
|
||||
epub_path = self.create_complex_epub()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
chapters = list(book.chapters)
|
||||
|
||||
# Check tables in chapter 4 (index 3)
|
||||
if len(chapters) > 3:
|
||||
chapter4_blocks = list(chapters[3].blocks)
|
||||
|
||||
# Find table blocks
|
||||
table_found = False
|
||||
code_block_found = False
|
||||
|
||||
for block in chapter4_blocks:
|
||||
if isinstance(block, Table):
|
||||
table_found = True
|
||||
|
||||
# Check table has rows
|
||||
rows = list(block.all_rows())
|
||||
self.assertGreater(len(rows), 0, "Table should have rows")
|
||||
|
||||
elif isinstance(block, CodeBlock):
|
||||
code_block_found = True
|
||||
|
||||
# Check code block has lines
|
||||
lines = list(block.lines())
|
||||
self.assertGreater(len(lines), 0, "Code block should have lines")
|
||||
|
||||
self.assertTrue(table_found, "Should find table in chapter 4")
|
||||
self.assertTrue(code_block_found, "Should find code block in chapter 4")
|
||||
|
||||
def test_epub_nested_content(self):
|
||||
"""Test that nested content structures are properly parsed."""
|
||||
epub_path = self.create_epub_with_nested_content()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
chapters = list(book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
chapter_blocks = list(chapters[0].blocks)
|
||||
self.assertGreater(len(chapter_blocks), 0)
|
||||
|
||||
# Should have multiple headings (h1, h2, h3, h4)
|
||||
headings = [block for block in chapter_blocks if isinstance(block, Heading)]
|
||||
self.assertGreater(len(headings), 2, "Should have multiple headings from nested content")
|
||||
|
||||
# Should have paragraphs and lists from nested content
|
||||
paragraphs = [block for block in chapter_blocks if isinstance(block, Paragraph)]
|
||||
lists = [block for block in chapter_blocks if isinstance(block, HList)]
|
||||
|
||||
self.assertGreater(len(paragraphs), 0, "Should have paragraphs from nested content")
|
||||
self.assertGreater(len(lists), 0, "Should have lists from nested content")
|
||||
|
||||
def test_epub_metadata_extraction(self):
|
||||
"""Test that EPUB metadata is properly extracted."""
|
||||
epub_path = self.create_complex_epub()
|
||||
book = read_epub(epub_path)
|
||||
|
||||
# Check basic metadata
|
||||
self.assertEqual(book.title, "Complex Test Book")
|
||||
|
||||
# Check that metadata was set (implementation may vary)
|
||||
# This tests that the metadata parsing doesn't crash
|
||||
self.assertIsNotNone(book.title)
|
||||
|
||||
def test_epub_reader_class_direct(self):
|
||||
"""Test EPUBReader class directly."""
|
||||
epub_path = self.create_simple_epub()
|
||||
|
||||
reader = EPUBReader(epub_path)
|
||||
book = reader.read()
|
||||
|
||||
self.assertIsInstance(book, Book)
|
||||
self.assertEqual(book.title, "Test Book")
|
||||
|
||||
def test_invalid_epub_handling(self):
|
||||
"""Test handling of invalid EPUB files."""
|
||||
# Create a non-EPUB file
|
||||
invalid_path = os.path.join(self.test_dir, 'invalid.epub')
|
||||
with open(invalid_path, 'w') as f:
|
||||
f.write("This is not an EPUB file")
|
||||
|
||||
# Should raise an exception or handle gracefully
|
||||
with self.assertRaises(Exception):
|
||||
read_epub(invalid_path)
|
||||
|
||||
def test_nonexistent_epub_handling(self):
|
||||
"""Test handling of nonexistent EPUB files."""
|
||||
nonexistent_path = os.path.join(self.test_dir, 'nonexistent.epub')
|
||||
|
||||
# Should raise an exception
|
||||
with self.assertRaises(Exception):
|
||||
read_epub(nonexistent_path)
|
||||
|
||||
def test_epub_with_custom_metadata(self):
|
||||
"""Test EPUB with various metadata fields."""
|
||||
book = epub.EpubBook()
|
||||
|
||||
# Set comprehensive metadata
|
||||
book.set_identifier('custom-metadata-test')
|
||||
book.set_title('Custom Metadata Test')
|
||||
book.set_language('en')
|
||||
book.add_author('Primary Author')
|
||||
book.add_author('Secondary Author')
|
||||
book.add_metadata('DC', 'description', 'A comprehensive test of metadata extraction')
|
||||
book.add_metadata('DC', 'subject', 'Testing')
|
||||
book.add_metadata('DC', 'subject', 'EPUB')
|
||||
book.add_metadata('DC', 'date', '2024-06-07')
|
||||
book.add_metadata('DC', 'publisher', 'Test Publishing House')
|
||||
book.add_metadata('DC', 'rights', 'Public Domain')
|
||||
|
||||
# Simple chapter
|
||||
chapter = epub.EpubHtml(
|
||||
title='Metadata Test',
|
||||
file_name='metadata.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter.content = '''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Metadata Test</title></head>
|
||||
<body>
|
||||
<h1>Metadata Test Chapter</h1>
|
||||
<p>This chapter tests metadata extraction.</p>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
book.add_item(chapter)
|
||||
book.toc = (epub.Link("metadata.xhtml", "Metadata Test", "meta"),)
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
book.spine = ['nav', chapter]
|
||||
|
||||
# Write and test
|
||||
epub_path = os.path.join(self.test_dir, f'test_metadata_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
# Read and verify
|
||||
parsed_book = read_epub(epub_path)
|
||||
self.assertEqual(parsed_book.title, "Custom Metadata Test")
|
||||
|
||||
# Verify chapters were created
|
||||
chapters = list(parsed_book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
|
||||
class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
|
||||
"""Test cases that specifically verify EPUB reader uses html_extraction properly."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.epub_files = []
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
for epub_file in self.epub_files:
|
||||
try:
|
||||
os.remove(epub_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if os.path.exists(self.test_dir):
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
|
||||
@unittest.skipUnless(EBOOKLIB_AVAILABLE, "ebooklib not available")
|
||||
def test_html_extraction_integration(self):
|
||||
"""Test that EPUB reader properly uses html_extraction functionality."""
|
||||
# Create an EPUB that exercises various HTML extraction features
|
||||
book = epub.EpubBook()
|
||||
book.set_identifier('html-extraction-test')
|
||||
book.set_title('HTML Extraction Test')
|
||||
book.set_language('en')
|
||||
book.add_author('Test Author')
|
||||
|
||||
# Chapter that exercises html_extraction features
|
||||
chapter = epub.EpubHtml(
|
||||
title='HTML Features',
|
||||
file_name='html_features.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter.content = '''
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>HTML Features</title></head>
|
||||
<body>
|
||||
<h1>HTML Extraction Test</h1>
|
||||
|
||||
<!-- Test paragraph with inline formatting -->
|
||||
<p>This paragraph has <strong>bold</strong>, <em>italic</em>,
|
||||
<u>underlined</u>, and <span style="color: #ff0000; font-weight: bold;">styled</span> text.</p>
|
||||
|
||||
<!-- Test headings -->
|
||||
<h2>Second Level Heading</h2>
|
||||
<h3>Third Level Heading</h3>
|
||||
|
||||
<!-- Test lists with styled content -->
|
||||
<ul>
|
||||
<li>Plain list item</li>
|
||||
<li><strong>Bold list item</strong></li>
|
||||
<li>List item with <em>italic text</em></li>
|
||||
</ul>
|
||||
|
||||
<!-- Test table with styled cells -->
|
||||
<table>
|
||||
<tr>
|
||||
<th style="font-weight: bold;">Header</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span style="color: blue;">Blue text</span></td>
|
||||
<td>Normal text</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Test blockquote -->
|
||||
<blockquote>
|
||||
<p>This is a quoted paragraph with <strong>bold text</strong>.</p>
|
||||
</blockquote>
|
||||
|
||||
<!-- Test code block -->
|
||||
<pre><code>def test_function():
|
||||
return "Hello, World!"</code></pre>
|
||||
|
||||
<!-- Test nested formatting -->
|
||||
<p>Nested formatting: <strong>bold with <em>italic nested</em> inside</strong>.</p>
|
||||
|
||||
<!-- Test color variations -->
|
||||
<p>
|
||||
<span style="color: red;">Red text</span>,
|
||||
<span style="color: #00ff00;">Green hex</span>,
|
||||
<span style="color: blue; text-decoration: underline;">Blue underlined</span>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
book.add_item(chapter)
|
||||
book.toc = (epub.Link("html_features.xhtml", "HTML Features", "html"),)
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
book.spine = ['nav', chapter]
|
||||
|
||||
# Write EPUB
|
||||
epub_path = os.path.join(self.test_dir, 'html_extraction_test.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
# Read and analyze
|
||||
parsed_book = read_epub(epub_path)
|
||||
chapters = list(parsed_book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
blocks = list(chapters[0].blocks)
|
||||
self.assertGreater(len(blocks), 5) # Should have multiple blocks
|
||||
|
||||
# Test that we get the expected block types
|
||||
block_types = [type(block).__name__ for block in blocks]
|
||||
self.assertIn('Heading', block_types, "Should have heading blocks")
|
||||
self.assertIn('Paragraph', block_types, "Should have paragraph blocks")
|
||||
self.assertIn('HList', block_types, "Should have list blocks")
|
||||
self.assertIn('Table', block_types, "Should have table blocks")
|
||||
self.assertIn('Quote', block_types, "Should have quote blocks")
|
||||
self.assertIn('CodeBlock', block_types, "Should have code blocks")
|
||||
|
||||
# Test styled content was preserved
|
||||
styled_content_found = False
|
||||
for block in blocks:
|
||||
if isinstance(block, Paragraph):
|
||||
words = list(block.words())
|
||||
for _, word in words:
|
||||
if (word.style.weight == FontWeight.BOLD or
|
||||
word.style.style == FontStyle.ITALIC or
|
||||
word.style.decoration == TextDecoration.UNDERLINE or
|
||||
word.style.colour != (0, 0, 0)):
|
||||
styled_content_found = True
|
||||
break
|
||||
if styled_content_found:
|
||||
break
|
||||
|
||||
self.assertTrue(styled_content_found, "Should find styled content in parsed blocks")
|
||||
|
||||
# Test specific color parsing
|
||||
red_text_found = False
|
||||
green_text_found = False
|
||||
blue_text_found = False
|
||||
|
||||
for block in blocks:
|
||||
if isinstance(block, (Paragraph, Table)):
|
||||
if isinstance(block, Paragraph):
|
||||
words = list(block.words())
|
||||
for _, word in words:
|
||||
if word.style.colour == (255, 0, 0): # Red
|
||||
red_text_found = True
|
||||
elif word.style.colour == (0, 255, 0): # Green
|
||||
green_text_found = True
|
||||
elif word.style.colour == (0, 0, 255): # Blue
|
||||
blue_text_found = True
|
||||
|
||||
# At least one color should be found (depending on implementation)
|
||||
color_found = red_text_found or green_text_found or blue_text_found
|
||||
self.assertTrue(color_found, "Should find at least one colored text")
|
||||
|
||||
|
||||
def test_epub_with_image(self):
|
||||
"""Test that images in EPUB are properly parsed."""
|
||||
book = epub.EpubBook()
|
||||
book.set_identifier('image-test-id')
|
||||
book.set_title('Image Test Book')
|
||||
book.set_language('en')
|
||||
book.add_author('Test Author')
|
||||
|
||||
# Create minimal JPEG data for testing
|
||||
img_data = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'9=82<.342\xff\xc0\x00\x11\x08\x00d\x00d\x01\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00\xaa\xff\xd9'
|
||||
|
||||
# Create an EpubImage item
|
||||
image_item = epub.EpubImage()
|
||||
image_item.id = 'test_img'
|
||||
image_item.file_name = 'images/test_image.jpg'
|
||||
image_item.media_type = 'image/jpeg'
|
||||
image_item.content = img_data
|
||||
|
||||
# Add image to book
|
||||
book.add_item(image_item)
|
||||
|
||||
# Create a chapter that references the image
|
||||
chapter = epub.EpubHtml(
|
||||
title='Image Chapter',
|
||||
file_name='image_chapter.xhtml',
|
||||
lang='en'
|
||||
)
|
||||
chapter.content = '''<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head><title>Image Chapter</title></head>
|
||||
<body>
|
||||
<h1>Chapter with Image</h1>
|
||||
<p>This chapter contains an image:</p>
|
||||
<img src="images/test_image.jpg" alt="Test image" width="300" height="200" />
|
||||
<p>Text after the image.</p>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
book.add_item(chapter)
|
||||
book.toc = (epub.Link("image_chapter.xhtml", "Image Chapter", "img_ch"),)
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
book.spine = ['nav', chapter]
|
||||
|
||||
# Write EPUB
|
||||
epub_path = os.path.join(self.test_dir, f'test_image_{len(self.epub_files)}.epub')
|
||||
epub.write_epub(epub_path, book, {})
|
||||
self.epub_files.append(epub_path)
|
||||
|
||||
# Read and analyze
|
||||
parsed_book = read_epub(epub_path)
|
||||
chapters = list(parsed_book.chapters)
|
||||
self.assertEqual(len(chapters), 1)
|
||||
|
||||
blocks = list(chapters[0].blocks)
|
||||
self.assertGreater(len(blocks), 0)
|
||||
|
||||
# Find blocks by type
|
||||
heading_blocks = [block for block in blocks if isinstance(block, Heading)]
|
||||
paragraph_blocks = [block for block in blocks if isinstance(block, Paragraph)]
|
||||
image_blocks = [block for block in blocks if isinstance(block, Image)]
|
||||
|
||||
# Verify we have the expected blocks
|
||||
self.assertEqual(len(heading_blocks), 1, "Should find exactly one heading block")
|
||||
self.assertGreaterEqual(len(paragraph_blocks), 2, "Should find at least two paragraph blocks")
|
||||
self.assertEqual(len(image_blocks), 1, "Should find exactly one image block")
|
||||
|
||||
# Verify image properties
|
||||
image_block = image_blocks[0]
|
||||
self.assertEqual(image_block.alt_text, "Test image")
|
||||
self.assertEqual(image_block.width, 300)
|
||||
self.assertEqual(image_block.height, 200)
|
||||
self.assertIn("test_image.jpg", image_block.source)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,564 @@
|
||||
"""
|
||||
Unit tests for HTML extraction functionality.
|
||||
|
||||
Tests the HTML parsing and conversion to pyWebLayout abstract elements,
|
||||
including styled content within paragraphs and block-level elements.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
||||
from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel, Quote, CodeBlock, HList, ListStyle, Table
|
||||
from pyWebLayout.abstract.document import Document
|
||||
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||
|
||||
|
||||
class TestHTMLParagraph(unittest.TestCase):
|
||||
"""Test cases for basic paragraph parsing."""
|
||||
|
||||
def test_simple(self):
|
||||
text = "<p>This is a paragraph.</p>"
|
||||
paragraphs = parse_html_string(text)
|
||||
self.assertEqual(len(paragraphs), 1)
|
||||
self.assertEqual(len(paragraphs[0]), 4)
|
||||
|
||||
for w1, t1 in zip(paragraphs[0].words(), "This is a paragraph.".split(" ")):
|
||||
self.assertEqual(w1[1].text, t1)
|
||||
|
||||
def test_multiple(self):
|
||||
text = "<p>This is a paragraph.</p><p>This is another paragraph.</p>"
|
||||
paragraphs = parse_html_string(text)
|
||||
self.assertEqual(len(paragraphs), 2)
|
||||
self.assertEqual(len(paragraphs[0]), 4)
|
||||
self.assertEqual(len(paragraphs[1]), 4)
|
||||
|
||||
for w1, t1 in zip(paragraphs[0].words(), "This is a paragraph.".split(" ")):
|
||||
self.assertEqual(w1[1].text, t1)
|
||||
|
||||
for w1, t1 in zip(paragraphs[1].words(), "This is another paragraph.".split(" ")):
|
||||
self.assertEqual(w1[1].text, t1)
|
||||
|
||||
|
||||
class TestHTMLStyledParagraphs(unittest.TestCase):
|
||||
"""Test cases for paragraphs with inline styling."""
|
||||
|
||||
def test_bold_text(self):
|
||||
"""Test paragraphs with bold text using <strong> and <b> tags."""
|
||||
text = "<p>This is <strong>bold text</strong> in a paragraph.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
words = list(blocks[0].words())
|
||||
self.assertEqual(len(words), 7) # "This is bold text in a paragraph."
|
||||
|
||||
# Check that 'bold' and 'text' words have bold font weight
|
||||
bold_word = words[2][1] # 'bold'
|
||||
text_word = words[3][1] # 'text'
|
||||
self.assertEqual(bold_word.text, "bold")
|
||||
self.assertEqual(bold_word.style.weight, FontWeight.BOLD)
|
||||
self.assertEqual(text_word.text, "text")
|
||||
self.assertEqual(text_word.style.weight, FontWeight.BOLD)
|
||||
|
||||
# Check that other words are not bold
|
||||
normal_word = words[0][1] # 'This'
|
||||
self.assertEqual(normal_word.text, "This")
|
||||
self.assertNotEqual(normal_word.style.weight, FontWeight.BOLD)
|
||||
|
||||
def test_italic_text(self):
|
||||
"""Test paragraphs with italic text using <em> and <i> tags."""
|
||||
text = "<p>This is <em>italic text</em> in a paragraph.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
words = list(blocks[0].words())
|
||||
|
||||
# Check that 'italic' and 'text' words have italic font style
|
||||
italic_word = words[2][1] # 'italic'
|
||||
text_word = words[3][1] # 'text'
|
||||
self.assertEqual(italic_word.text, "italic")
|
||||
self.assertEqual(italic_word.style.style, FontStyle.ITALIC)
|
||||
self.assertEqual(text_word.text, "text")
|
||||
self.assertEqual(text_word.style.style, FontStyle.ITALIC)
|
||||
|
||||
def test_underlined_text(self):
|
||||
"""Test paragraphs with underlined text using <u> tag."""
|
||||
text = "<p>This is <u>underlined text</u> here.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
|
||||
words = list(blocks[0].words())
|
||||
underlined_word = words[2][1] # 'underlined'
|
||||
self.assertEqual(underlined_word.style.decoration, TextDecoration.UNDERLINE)
|
||||
|
||||
def test_strikethrough_text(self):
|
||||
"""Test paragraphs with strikethrough text using <s> and <del> tags."""
|
||||
text = "<p>This is <s>strikethrough text</s> here.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
|
||||
words = list(blocks[0].words())
|
||||
strike_word = words[2][1] # 'strikethrough'
|
||||
self.assertEqual(strike_word.style.decoration, TextDecoration.STRIKETHROUGH)
|
||||
|
||||
def test_span_with_inline_styles(self):
|
||||
"""Test paragraphs with span elements containing inline CSS styles."""
|
||||
text = '<p>This text is normal, but <span style="color: red; font-weight: bold;">this part is red and bold</span>.</p>'
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
words = list(blocks[0].words())
|
||||
|
||||
# Find the styled words
|
||||
styled_words = []
|
||||
for _, word in words:
|
||||
if word.text in ["this", "part", "is", "red", "and", "bold"]:
|
||||
if word.style.weight == FontWeight.BOLD:
|
||||
styled_words.append(word)
|
||||
|
||||
self.assertGreater(len(styled_words), 0, "Should have bold words in styled span")
|
||||
|
||||
# Check that at least one word has the red color
|
||||
red_words = [w for w in styled_words if w.style.colour == (255, 0, 0)]
|
||||
self.assertGreater(len(red_words), 0, "Should have red colored words")
|
||||
|
||||
def test_mixed_formatting(self):
|
||||
"""Test paragraphs with multiple formatting elements combined."""
|
||||
text = "<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style=\"color: blue;\">blue</span>, and <mark>highlighted</mark> text all together.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
words = list(blocks[0].words())
|
||||
|
||||
# Check for bold word
|
||||
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
|
||||
self.assertGreater(len(bold_words), 0, "Should have bold words")
|
||||
|
||||
# Check for italic word
|
||||
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(len(italic_words), 0, "Should have italic words")
|
||||
|
||||
# Check for blue colored word
|
||||
blue_words = [w for _, w in words if w.style.colour == (0, 0, 255)]
|
||||
self.assertGreater(len(blue_words), 0, "Should have blue colored words")
|
||||
|
||||
def test_nested_formatting(self):
|
||||
"""Test nested formatting elements."""
|
||||
text = "<p>This has <strong>bold with <em>italic inside</em></strong> formatting.</p>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
|
||||
words = list(blocks[0].words())
|
||||
|
||||
# Find words that should be both bold and italic
|
||||
bold_italic_words = [w for _, w in words
|
||||
if w.style.weight == FontWeight.BOLD and w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(len(bold_italic_words), 0, "Should have words that are both bold and italic")
|
||||
|
||||
def test_color_variations(self):
|
||||
"""Test different color formats in CSS."""
|
||||
text = '<p><span style="color: #ff0000;">Hex red</span> and <span style="color: green;">Named green</span>.</p>'
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
|
||||
words = list(blocks[0].words())
|
||||
|
||||
# Check for hex red color
|
||||
hex_red_words = [w for _, w in words if w.style.colour == (255, 0, 0)]
|
||||
self.assertGreater(len(hex_red_words), 0, "Should have hex red colored words")
|
||||
|
||||
# Check for named green color
|
||||
green_words = [w for _, w in words if w.style.colour == (0, 255, 0)]
|
||||
self.assertGreater(len(green_words), 0, "Should have green colored words")
|
||||
|
||||
|
||||
class TestHTMLBlockElements(unittest.TestCase):
|
||||
"""Test cases for block-level HTML elements."""
|
||||
|
||||
def test_body_element(self):
|
||||
"""Test parsing of body element containing other elements."""
|
||||
text = "<body><p>Paragraph one.</p><p>Paragraph two.</p></body>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 2)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
self.assertIsInstance(blocks[1], Paragraph)
|
||||
|
||||
def test_div_container(self):
|
||||
"""Test div elements as generic containers."""
|
||||
text = "<div><p>First paragraph.</p><p>Second paragraph.</p></div>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 2)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
self.assertIsInstance(blocks[1], Paragraph)
|
||||
|
||||
def test_headings(self):
|
||||
"""Test all heading levels h1-h6."""
|
||||
text = "<h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 6)
|
||||
|
||||
expected_levels = [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3,
|
||||
HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]
|
||||
|
||||
for i, block in enumerate(blocks):
|
||||
self.assertIsInstance(block, Heading)
|
||||
self.assertEqual(block.level, expected_levels[i])
|
||||
|
||||
words = list(block.words())
|
||||
self.assertEqual(len(words), 2) # "Heading" and number
|
||||
self.assertEqual(words[0][1].text, "Heading")
|
||||
|
||||
def test_blockquote(self):
|
||||
"""Test blockquote elements."""
|
||||
text = "<blockquote><p>This is a quoted paragraph.</p></blockquote>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Quote)
|
||||
|
||||
# Check that the quote contains a paragraph
|
||||
quote_blocks = list(blocks[0].blocks())
|
||||
self.assertEqual(len(quote_blocks), 1)
|
||||
self.assertIsInstance(quote_blocks[0], Paragraph)
|
||||
|
||||
def test_preformatted_code(self):
|
||||
"""Test preformatted code blocks."""
|
||||
text = "<pre><code>function hello() {\n console.log('Hello');\n}</code></pre>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], CodeBlock)
|
||||
|
||||
lines = list(blocks[0].lines())
|
||||
self.assertGreater(len(lines), 0)
|
||||
|
||||
def test_unordered_list(self):
|
||||
"""Test unordered lists."""
|
||||
text = "<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], HList)
|
||||
self.assertEqual(blocks[0].style, ListStyle.UNORDERED)
|
||||
|
||||
items = list(blocks[0].items())
|
||||
self.assertEqual(len(items), 3)
|
||||
|
||||
def test_ordered_list(self):
|
||||
"""Test ordered lists."""
|
||||
text = "<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], HList)
|
||||
self.assertEqual(blocks[0].style, ListStyle.ORDERED)
|
||||
|
||||
def test_list_with_styled_content(self):
|
||||
"""Test lists containing styled content."""
|
||||
text = "<ul><li>Normal item</li><li><strong>Bold item</strong></li><li>Item with <em>italic</em> text</li></ul>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], HList)
|
||||
|
||||
items = list(blocks[0].items())
|
||||
self.assertEqual(len(items), 3)
|
||||
|
||||
# Check second item has bold text
|
||||
second_item_blocks = list(items[1].blocks())
|
||||
if second_item_blocks:
|
||||
words = list(second_item_blocks[0].words())
|
||||
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
|
||||
self.assertGreater(len(bold_words), 0)
|
||||
|
||||
def test_table_basic(self):
|
||||
"""Test basic table structure."""
|
||||
text = """
|
||||
<table>
|
||||
<tr>
|
||||
<th>Header 1</th>
|
||||
<th>Header 2</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell 1</td>
|
||||
<td>Cell 2</td>
|
||||
</tr>
|
||||
</table>
|
||||
"""
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Table)
|
||||
|
||||
def test_semantic_elements(self):
|
||||
"""Test semantic HTML5 elements treated as containers."""
|
||||
text = "<section><article><p>Article content</p></article></section>"
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
def test_nested_block_elements(self):
|
||||
"""Test nested block elements."""
|
||||
text = """
|
||||
<div>
|
||||
<h2>Section Title</h2>
|
||||
<p>Some introductory text.</p>
|
||||
<blockquote>
|
||||
<p>A quoted paragraph.</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
"""
|
||||
blocks = parse_html_string(text)
|
||||
self.assertGreater(len(blocks), 2)
|
||||
|
||||
# Should have at least a heading, paragraph, and quote
|
||||
has_heading = any(isinstance(b, Heading) for b in blocks)
|
||||
has_paragraph = any(isinstance(b, Paragraph) for b in blocks)
|
||||
has_quote = any(isinstance(b, Quote) for b in blocks)
|
||||
|
||||
self.assertTrue(has_heading, "Should contain a heading")
|
||||
self.assertTrue(has_paragraph, "Should contain a paragraph")
|
||||
self.assertTrue(has_quote, "Should contain a quote")
|
||||
|
||||
def test_empty_elements(self):
|
||||
"""Test handling of empty elements."""
|
||||
text = "<p></p><div></div><span></span>"
|
||||
blocks = parse_html_string(text)
|
||||
# Empty elements may not create blocks, which is acceptable behavior
|
||||
self.assertGreaterEqual(len(blocks), 0)
|
||||
|
||||
# Test that empty paragraph with some content does create a block
|
||||
text_with_content = "<p> </p>" # Contains whitespace
|
||||
blocks_with_content = parse_html_string(text_with_content)
|
||||
# This should create at least one block since there's whitespace content
|
||||
self.assertGreaterEqual(len(blocks_with_content), 0)
|
||||
|
||||
|
||||
class TestHTMLComplexStructures(unittest.TestCase):
|
||||
"""Test cases for complex HTML structures combining multiple features."""
|
||||
|
||||
def test_article_with_mixed_content(self):
|
||||
"""Test a realistic article structure with mixed content."""
|
||||
text = """
|
||||
<article>
|
||||
<h1>Article Title</h1>
|
||||
<p>This is the <strong>introduction</strong> paragraph with <em>some emphasis</em>.</p>
|
||||
<blockquote>
|
||||
<p>This is a <span style="color: blue;">quoted section</span> with styling.</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>First <strong>important</strong> point</li>
|
||||
<li>Second point with <code>inline code</code></li>
|
||||
</ul>
|
||||
</article>
|
||||
"""
|
||||
blocks = parse_html_string(text)
|
||||
self.assertGreater(len(blocks), 3)
|
||||
|
||||
# Verify we have the expected block types
|
||||
block_types = [type(b).__name__ for b in blocks]
|
||||
self.assertIn('Heading', block_types)
|
||||
self.assertIn('Paragraph', block_types)
|
||||
self.assertIn('Quote', block_types)
|
||||
self.assertIn('HList', block_types)
|
||||
|
||||
def test_styled_table_content(self):
|
||||
"""Test table with styled cell content."""
|
||||
text = """
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Product</strong></th>
|
||||
<th><em>Price</em></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Item with <span style="color: red;">red text</span></td>
|
||||
<td><strong>$19.99</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
blocks = parse_html_string(text)
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Table)
|
||||
|
||||
|
||||
class TestHTMLFontRegistryIntegration(unittest.TestCase):
|
||||
"""Test cases for font registry integration with HTML extraction."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.doc = Document("Test Document", "en-US")
|
||||
self.base_font = Font(font_size=16, colour=(0, 0, 0))
|
||||
|
||||
def test_font_registry_creates_fonts(self):
|
||||
"""Test that HTML parsing with document context creates fonts in registry."""
|
||||
html_content = """
|
||||
<div>
|
||||
<p>This is <strong>bold text</strong> and <em>italic text</em>.</p>
|
||||
<h1>Main Header</h1>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Initially empty style registry
|
||||
initial_style_count = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
# Parse HTML with document context
|
||||
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
|
||||
# Should have created styles for different formatting
|
||||
final_style_count = self.doc.get_style_registry().get_style_count()
|
||||
self.assertGreater(final_style_count, initial_style_count,
|
||||
"Should have created styles in registry")
|
||||
|
||||
# Should have created blocks
|
||||
self.assertGreater(len(blocks), 0, "Should have created blocks")
|
||||
|
||||
def test_font_registry_reuses_fonts(self):
|
||||
"""Test that parsing same content reuses existing styles."""
|
||||
html_content = """
|
||||
<div>
|
||||
<p>This is <strong>bold text</strong> and <em>italic text</em>.</p>
|
||||
<h1>Main Header</h1>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# First parse
|
||||
blocks1 = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
first_parse_style_count = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
# Second parse with same content
|
||||
blocks2 = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
second_parse_style_count = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
# Style count should not increase on second parse
|
||||
self.assertEqual(first_parse_style_count, second_parse_style_count,
|
||||
"Should reuse existing styles instead of creating new ones")
|
||||
|
||||
# Both parses should create same number of blocks
|
||||
self.assertEqual(len(blocks1), len(blocks2),
|
||||
"Should create same structure on both parses")
|
||||
|
||||
def test_font_registry_different_styles_create_different_fonts(self):
|
||||
"""Test that different styles create different style objects."""
|
||||
# Create styles with different properties
|
||||
style_id1, style1 = self.doc.get_or_create_style(
|
||||
font_size=14, color=(255, 0, 0), font_weight=FontWeight.BOLD
|
||||
)
|
||||
style_id2, style2 = self.doc.get_or_create_style(
|
||||
font_size=16, color=(255, 0, 0), font_weight=FontWeight.BOLD
|
||||
)
|
||||
style_id3, style3 = self.doc.get_or_create_style(
|
||||
font_size=14, color=(0, 255, 0), font_weight=FontWeight.BOLD
|
||||
)
|
||||
|
||||
# Should be different style IDs
|
||||
self.assertNotEqual(style_id1, style_id2, "Different sizes should create different styles")
|
||||
self.assertNotEqual(style_id1, style_id3, "Different colors should create different styles")
|
||||
self.assertNotEqual(style_id2, style_id3, "All styles should be different")
|
||||
|
||||
# Should have multiple styles in registry
|
||||
self.assertGreaterEqual(self.doc.get_style_registry().get_style_count(), 3)
|
||||
|
||||
def test_font_registry_integration_with_html_styles(self):
|
||||
"""Test that HTML parsing uses style registry for styled content."""
|
||||
html_content = """
|
||||
<p>Normal text with <strong>bold</strong> and <em>italic</em> and
|
||||
<span style="color: red;">red text</span>.</p>
|
||||
"""
|
||||
|
||||
# Parse content
|
||||
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
|
||||
# Extract all words from the paragraph
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words())
|
||||
|
||||
# Find words with different styles
|
||||
normal_words = [w for _, w in words if w.style.weight == FontWeight.NORMAL
|
||||
and w.style.style == FontStyle.NORMAL]
|
||||
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
|
||||
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
|
||||
red_words = [w for _, w in words if w.style.colour == (255, 0, 0)]
|
||||
|
||||
# Should have words with different styles
|
||||
self.assertGreater(len(normal_words), 0, "Should have normal words")
|
||||
self.assertGreater(len(bold_words), 0, "Should have bold words")
|
||||
self.assertGreater(len(italic_words), 0, "Should have italic words")
|
||||
self.assertGreater(len(red_words), 0, "Should have red words")
|
||||
|
||||
# Style registry should contain multiple styles for different formatting
|
||||
self.assertGreater(self.doc.get_style_registry().get_style_count(), 1,
|
||||
"Should have multiple styles for different formatting")
|
||||
|
||||
def test_font_registry_without_document_context(self):
|
||||
"""Test that parsing without document context works (fallback behavior)."""
|
||||
html_content = "<p>This is <strong>bold text</strong>.</p>"
|
||||
|
||||
# Get initial style count (should include default style)
|
||||
initial_style_count = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
# Parse without document context
|
||||
blocks = parse_html_string(html_content, self.base_font)
|
||||
|
||||
# Should still create blocks successfully
|
||||
self.assertEqual(len(blocks), 1)
|
||||
self.assertIsInstance(blocks[0], Paragraph)
|
||||
|
||||
# Should not affect document's style registry
|
||||
final_style_count = self.doc.get_style_registry().get_style_count()
|
||||
self.assertEqual(final_style_count, initial_style_count,
|
||||
"Document style registry should remain unchanged")
|
||||
|
||||
def test_complex_html_font_reuse(self):
|
||||
"""Test style reuse with complex HTML containing repeated styles."""
|
||||
html_content = """
|
||||
<div>
|
||||
<h1>First Header</h1>
|
||||
<p>Paragraph with <strong>bold</strong> text.</p>
|
||||
<h1>Second Header</h1>
|
||||
<p>Another paragraph with <strong>bold</strong> text.</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Parse content
|
||||
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
style_count_after_parse = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
# Parse same content again
|
||||
blocks2 = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
style_count_after_second_parse = self.doc.get_style_registry().get_style_count()
|
||||
|
||||
# Style count should not increase on second parse
|
||||
self.assertEqual(style_count_after_parse, style_count_after_second_parse,
|
||||
"Styles should be reused for repeated formatting")
|
||||
|
||||
# Both should create same structure
|
||||
self.assertEqual(len(blocks), len(blocks2))
|
||||
|
||||
def test_font_registry_with_nested_styles(self):
|
||||
"""Test style registry with nested HTML styles."""
|
||||
html_content = """
|
||||
<p>Text with <strong>bold and <em>bold italic</em> nested</strong> styles.</p>
|
||||
"""
|
||||
|
||||
# Parse content
|
||||
blocks = parse_html_string(html_content, self.base_font, document=self.doc)
|
||||
|
||||
# Should create styles for different style combinations
|
||||
paragraph = blocks[0]
|
||||
words = list(paragraph.words())
|
||||
|
||||
# Find words that are both bold and italic
|
||||
bold_italic_words = [w for _, w in words
|
||||
if w.style.weight == FontWeight.BOLD
|
||||
and w.style.style == FontStyle.ITALIC]
|
||||
|
||||
self.assertGreater(len(bold_italic_words), 0,
|
||||
"Should have words with combined bold+italic style")
|
||||
|
||||
# Should have multiple styles in registry for different combinations
|
||||
self.assertGreater(self.doc.get_style_registry().get_style_count(), 1,
|
||||
"Should create separate styles for style combinations")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,493 @@
|
||||
"""
|
||||
Unit tests for individual HTML extraction functions.
|
||||
|
||||
Tests the specific handler functions and utility functions in html_extraction module,
|
||||
reusing test patterns from test_html_extraction.py that are known to pass.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from pyWebLayout.io.readers.html_extraction import (
|
||||
create_base_context,
|
||||
apply_element_styling,
|
||||
parse_inline_styles,
|
||||
apply_element_font_styles,
|
||||
extract_text_content,
|
||||
paragraph_handler,
|
||||
div_handler,
|
||||
heading_handler,
|
||||
blockquote_handler,
|
||||
preformatted_handler,
|
||||
unordered_list_handler,
|
||||
ordered_list_handler,
|
||||
list_item_handler,
|
||||
table_handler,
|
||||
table_row_handler,
|
||||
table_cell_handler,
|
||||
table_header_cell_handler,
|
||||
horizontal_rule_handler,
|
||||
image_handler,
|
||||
StyleContext,
|
||||
)
|
||||
from pyWebLayout.abstract.block import (
|
||||
Paragraph,
|
||||
Heading,
|
||||
HeadingLevel,
|
||||
Quote,
|
||||
CodeBlock,
|
||||
HList,
|
||||
ListItem,
|
||||
ListStyle,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
HorizontalRule,
|
||||
Image,
|
||||
)
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||
|
||||
|
||||
class TestUtilityFunctions(unittest.TestCase):
|
||||
"""Test cases for utility functions."""
|
||||
|
||||
def test_create_base_context(self):
|
||||
"""Test creation of base style context."""
|
||||
context = create_base_context()
|
||||
|
||||
self.assertIsInstance(context, StyleContext)
|
||||
self.assertIsInstance(context.font, Font)
|
||||
self.assertIsNone(context.background)
|
||||
self.assertEqual(context.css_classes, set())
|
||||
self.assertEqual(context.css_styles, {})
|
||||
self.assertEqual(context.element_attributes, {})
|
||||
self.assertEqual(context.parent_elements, [])
|
||||
|
||||
def test_parse_inline_styles_from_existing_tests(self):
|
||||
"""Test parsing CSS inline styles - adapted from test_span_with_inline_styles."""
|
||||
# From: '<span style="color: red; font-weight: bold;">this part is red and bold</span>'
|
||||
style_text = "color: red; font-weight: bold;"
|
||||
styles = parse_inline_styles(style_text)
|
||||
|
||||
expected = {
|
||||
"color": "red",
|
||||
"font-weight": "bold"
|
||||
}
|
||||
self.assertEqual(styles, expected)
|
||||
|
||||
def test_parse_inline_styles_color_variations(self):
|
||||
"""Test parsing different color formats - adapted from test_color_variations."""
|
||||
# Test hex color parsing
|
||||
hex_style = "color: #ff0000;"
|
||||
styles = parse_inline_styles(hex_style)
|
||||
self.assertEqual(styles.get("color"), "#ff0000")
|
||||
|
||||
# Test named color parsing
|
||||
named_style = "color: green;"
|
||||
styles = parse_inline_styles(named_style)
|
||||
self.assertEqual(styles.get("color"), "green")
|
||||
|
||||
def test_apply_element_font_styles_bold_elements(self):
|
||||
"""Test font style application for bold elements - adapted from test_bold_text."""
|
||||
base_font = Font()
|
||||
|
||||
# Test <strong> tag - from "<strong>bold text</strong>"
|
||||
font = apply_element_font_styles(base_font, "strong", {})
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
|
||||
# Test <b> tag
|
||||
font = apply_element_font_styles(base_font, "b", {})
|
||||
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||
|
||||
def test_apply_element_font_styles_italic_elements(self):
|
||||
"""Test font style application for italic elements - adapted from test_italic_text."""
|
||||
base_font = Font()
|
||||
|
||||
# Test <em> tag - from "<em>italic text</em>"
|
||||
font = apply_element_font_styles(base_font, "em", {})
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
|
||||
# Test <i> tag
|
||||
font = apply_element_font_styles(base_font, "i", {})
|
||||
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||
|
||||
def test_apply_element_font_styles_decoration_elements(self):
|
||||
"""Test font decoration - adapted from test_underlined_text and test_strikethrough_text."""
|
||||
base_font = Font()
|
||||
|
||||
# Test <u> tag - from "<u>underlined text</u>"
|
||||
font = apply_element_font_styles(base_font, "u", {})
|
||||
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||
|
||||
# Test <s> tag - from "<s>strikethrough text</s>"
|
||||
font = apply_element_font_styles(base_font, "s", {})
|
||||
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
|
||||
|
||||
# Test <del> tag
|
||||
font = apply_element_font_styles(base_font, "del", {})
|
||||
self.assertEqual(font.decoration, TextDecoration.STRIKETHROUGH)
|
||||
|
||||
def test_apply_element_font_styles_headings(self):
|
||||
"""Test heading font styles - adapted from test_headings."""
|
||||
base_font = Font()
|
||||
|
||||
# Test heading sizes and weights - from test_headings which tests h1-h6
|
||||
headings = [("h1", 24), ("h2", 20), ("h3", 18), ("h4", 16), ("h5", 14), ("h6", 12)]
|
||||
|
||||
for tag, expected_size in headings:
|
||||
font = apply_element_font_styles(base_font, tag, {})
|
||||
self.assertEqual(font.font_size, expected_size, f"Size mismatch for {tag}")
|
||||
self.assertEqual(font.weight, FontWeight.BOLD, f"Weight should be bold for {tag}")
|
||||
|
||||
def test_apply_element_font_styles_color_parsing(self):
|
||||
"""Test color parsing - adapted from test_color_variations."""
|
||||
base_font = Font()
|
||||
|
||||
# Test named colors - from '<span style="color: green;">Named green</span>'
|
||||
css_styles = {"color": "green"}
|
||||
font = apply_element_font_styles(base_font, "span", css_styles)
|
||||
self.assertEqual(font.colour, (0, 255, 0))
|
||||
|
||||
# Test hex colors - from '<span style="color: #ff0000;">Hex red</span>'
|
||||
css_styles = {"color": "#ff0000"}
|
||||
font = apply_element_font_styles(base_font, "span", css_styles)
|
||||
self.assertEqual(font.colour, (255, 0, 0))
|
||||
|
||||
def test_apply_element_styling_with_classes_and_styles(self):
|
||||
"""Test complete element styling - adapted from test_span_with_inline_styles."""
|
||||
# From: '<span style="color: red; font-weight: bold;">this part is red and bold</span>'
|
||||
soup = BeautifulSoup('<span class="highlight" style="color: red; font-weight: bold;">text</span>', 'html.parser')
|
||||
element = soup.find('span')
|
||||
base_context = create_base_context()
|
||||
|
||||
styled_context = apply_element_styling(base_context, element)
|
||||
|
||||
# Check CSS classes
|
||||
self.assertIn("highlight", styled_context.css_classes)
|
||||
|
||||
# Check CSS styles
|
||||
self.assertEqual(styled_context.css_styles.get("color"), "red")
|
||||
self.assertEqual(styled_context.css_styles.get("font-weight"), "bold")
|
||||
|
||||
# Check font styling
|
||||
self.assertEqual(styled_context.font.colour, (255, 0, 0))
|
||||
self.assertEqual(styled_context.font.weight, FontWeight.BOLD)
|
||||
|
||||
|
||||
class TestExtractTextContent(unittest.TestCase):
|
||||
"""Test cases for text content extraction."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.base_context = create_base_context()
|
||||
|
||||
def test_extract_simple_text(self):
|
||||
"""Test extracting simple text - adapted from test_simple."""
|
||||
# From: "<p>This is a paragraph.</p>"
|
||||
soup = BeautifulSoup('<p>This is a paragraph.</p>', 'html.parser')
|
||||
element = soup.find('p')
|
||||
|
||||
words = extract_text_content(element, self.base_context)
|
||||
|
||||
# Should match the expected word count from original test
|
||||
self.assertEqual(len(words), 4) # "This", "is", "a", "paragraph."
|
||||
self.assertIsInstance(words[0], Word)
|
||||
self.assertEqual(words[0].text, "This")
|
||||
|
||||
def test_extract_styled_text_bold(self):
|
||||
"""Test extracting bold styled text - adapted from test_bold_text."""
|
||||
# From: "<p>This is <strong>bold text</strong> in a paragraph.</p>"
|
||||
soup = BeautifulSoup('<span>This is <strong>bold text</strong> in a paragraph.</span>', 'html.parser')
|
||||
element = soup.find('span')
|
||||
|
||||
words = extract_text_content(element, self.base_context)
|
||||
|
||||
# Find the bold words
|
||||
bold_words = [w for w in words if w.style.weight == FontWeight.BOLD]
|
||||
self.assertGreater(len(bold_words), 0, "Should have bold words")
|
||||
|
||||
# Check specific words are bold (from original test expectations)
|
||||
bold_word_texts = [w.text for w in bold_words]
|
||||
self.assertIn("bold", bold_word_texts)
|
||||
self.assertIn("text", bold_word_texts)
|
||||
|
||||
def test_extract_nested_formatting(self):
|
||||
"""Test nested formatting - adapted from test_nested_formatting."""
|
||||
# From: "<p>This has <strong>bold with <em>italic inside</em></strong> formatting.</p>"
|
||||
soup = BeautifulSoup('<span>This has <strong>bold with <em>italic inside</em></strong> formatting.</span>', 'html.parser')
|
||||
element = soup.find('span')
|
||||
|
||||
words = extract_text_content(element, self.base_context)
|
||||
|
||||
# Find words that should be both bold and italic
|
||||
bold_italic_words = [w for w in words
|
||||
if w.style.weight == FontWeight.BOLD and w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(len(bold_italic_words), 0, "Should have words that are both bold and italic")
|
||||
|
||||
|
||||
class TestHandlerFunctions(unittest.TestCase):
|
||||
"""Test cases for HTML element handler functions using known working patterns."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.base_context = create_base_context()
|
||||
|
||||
def test_paragraph_handler_simple(self):
|
||||
"""Test paragraph handler - adapted from test_simple."""
|
||||
# From: "<p>This is a paragraph.</p>"
|
||||
soup = BeautifulSoup('<p>This is a paragraph.</p>', 'html.parser')
|
||||
element = soup.find('p')
|
||||
|
||||
result = paragraph_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, Paragraph)
|
||||
# Should match original test expectations
|
||||
self.assertEqual(len(result), 4) # 4 words
|
||||
|
||||
words = list(result.words())
|
||||
expected_texts = ["This", "is", "a", "paragraph."]
|
||||
for i, expected_text in enumerate(expected_texts):
|
||||
self.assertEqual(words[i][1].text, expected_text)
|
||||
|
||||
def test_heading_handler_all_levels(self):
|
||||
"""Test heading handler - adapted from test_headings."""
|
||||
# From: "<h1>Heading 1</h1><h2>Heading 2</h2>..."
|
||||
expected_levels = [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3,
|
||||
HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]
|
||||
|
||||
for i, expected_level in enumerate(expected_levels, 1):
|
||||
tag = f"h{i}"
|
||||
soup = BeautifulSoup(f'<{tag}>Heading {i}</{tag}>', 'html.parser')
|
||||
element = soup.find(tag)
|
||||
|
||||
result = heading_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, Heading)
|
||||
self.assertEqual(result.level, expected_level)
|
||||
|
||||
# Should match original test word expectations
|
||||
words = list(result.words())
|
||||
self.assertEqual(len(words), 2) # "Heading" and number
|
||||
self.assertEqual(words[0][1].text, "Heading")
|
||||
|
||||
def test_blockquote_handler(self):
|
||||
"""Test blockquote handler - adapted from test_blockquote."""
|
||||
# From: "<blockquote><p>This is a quoted paragraph.</p></blockquote>"
|
||||
soup = BeautifulSoup('<blockquote><p>This is a quoted paragraph.</p></blockquote>', 'html.parser')
|
||||
element = soup.find('blockquote')
|
||||
|
||||
result = blockquote_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, Quote)
|
||||
|
||||
# Check that the quote contains a paragraph (from original test)
|
||||
quote_blocks = list(result.blocks())
|
||||
self.assertEqual(len(quote_blocks), 1)
|
||||
self.assertIsInstance(quote_blocks[0], Paragraph)
|
||||
|
||||
def test_preformatted_handler(self):
|
||||
"""Test preformatted handler - adapted from test_preformatted_code."""
|
||||
# From: "<pre><code>function hello() {\n console.log('Hello');\n}</code></pre>"
|
||||
soup = BeautifulSoup('<pre><code>function hello() {\n console.log(\'Hello\');\n}</code></pre>', 'html.parser')
|
||||
element = soup.find('pre')
|
||||
|
||||
result = preformatted_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, CodeBlock)
|
||||
|
||||
# Should have lines (from original test expectation)
|
||||
lines = list(result.lines())
|
||||
self.assertGreater(len(lines), 0)
|
||||
|
||||
def test_unordered_list_handler(self):
|
||||
"""Test unordered list handler - adapted from test_unordered_list."""
|
||||
# From: "<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>"
|
||||
soup = BeautifulSoup('<ul><li>First item</li><li>Second item</li><li>Third item</li></ul>', 'html.parser')
|
||||
element = soup.find('ul')
|
||||
|
||||
result = unordered_list_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, HList)
|
||||
self.assertEqual(result.style, ListStyle.UNORDERED)
|
||||
|
||||
# Should match original test expectations
|
||||
items = list(result.items())
|
||||
self.assertEqual(len(items), 3)
|
||||
|
||||
def test_ordered_list_handler(self):
|
||||
"""Test ordered list handler - adapted from test_ordered_list."""
|
||||
# From: "<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>"
|
||||
soup = BeautifulSoup('<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>', 'html.parser')
|
||||
element = soup.find('ol')
|
||||
|
||||
result = ordered_list_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, HList)
|
||||
self.assertEqual(result.style, ListStyle.ORDERED)
|
||||
|
||||
# Should match original test expectations
|
||||
items = list(result.items())
|
||||
self.assertEqual(len(items), 3) # "First item", "Second item", "Third item"
|
||||
|
||||
def test_list_item_handler(self):
|
||||
"""Test list item handler."""
|
||||
soup = BeautifulSoup('<li>List item content</li>', 'html.parser')
|
||||
element = soup.find('li')
|
||||
|
||||
result = list_item_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, ListItem)
|
||||
blocks = list(result.blocks())
|
||||
self.assertGreater(len(blocks), 0)
|
||||
|
||||
def test_table_handler(self):
|
||||
"""Test table handler - adapted from test_table_basic."""
|
||||
# From test_table_basic structure
|
||||
soup = BeautifulSoup('''
|
||||
<table>
|
||||
<tr>
|
||||
<th>Header 1</th>
|
||||
<th>Header 2</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell 1</td>
|
||||
<td>Cell 2</td>
|
||||
</tr>
|
||||
</table>
|
||||
''', 'html.parser')
|
||||
element = soup.find('table')
|
||||
|
||||
result = table_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, Table)
|
||||
|
||||
def test_table_row_handler(self):
|
||||
"""Test table row handler."""
|
||||
soup = BeautifulSoup('<tr><td>Cell 1</td><td>Cell 2</td></tr>', 'html.parser')
|
||||
element = soup.find('tr')
|
||||
|
||||
result = table_row_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, TableRow)
|
||||
|
||||
def test_table_cell_handler(self):
|
||||
"""Test table cell handler."""
|
||||
soup = BeautifulSoup('<td>Cell content</td>', 'html.parser')
|
||||
element = soup.find('td')
|
||||
|
||||
# Apply styling to get attributes
|
||||
styled_context = apply_element_styling(self.base_context, element)
|
||||
result = table_cell_handler(element, styled_context)
|
||||
|
||||
self.assertIsInstance(result, TableCell)
|
||||
self.assertEqual(result.is_header, False)
|
||||
|
||||
def test_table_header_cell_handler(self):
|
||||
"""Test table header cell handler."""
|
||||
soup = BeautifulSoup('<th>Header content</th>', 'html.parser')
|
||||
element = soup.find('th')
|
||||
|
||||
# Apply styling to get attributes
|
||||
styled_context = apply_element_styling(self.base_context, element)
|
||||
result = table_header_cell_handler(element, styled_context)
|
||||
|
||||
self.assertIsInstance(result, TableCell)
|
||||
self.assertEqual(result.is_header, True)
|
||||
|
||||
def test_horizontal_rule_handler(self):
|
||||
"""Test horizontal rule handler."""
|
||||
soup = BeautifulSoup('<hr>', 'html.parser')
|
||||
element = soup.find('hr')
|
||||
|
||||
result = horizontal_rule_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, HorizontalRule)
|
||||
|
||||
def test_image_handler(self):
|
||||
"""Test image handler."""
|
||||
soup = BeautifulSoup('<img src="test.jpg" alt="Test image" width="100" height="50">', 'html.parser')
|
||||
element = soup.find('img')
|
||||
|
||||
# Need to apply styling first to get attributes
|
||||
styled_context = apply_element_styling(self.base_context, element)
|
||||
result = image_handler(element, styled_context)
|
||||
|
||||
self.assertIsInstance(result, Image)
|
||||
self.assertEqual(result.source, "test.jpg")
|
||||
self.assertEqual(result.alt_text, "Test image")
|
||||
self.assertEqual(result.width, 100)
|
||||
self.assertEqual(result.height, 50)
|
||||
|
||||
def test_div_handler_container(self):
|
||||
"""Test div handler - adapted from test_div_container."""
|
||||
# From: "<div><p>First paragraph.</p><p>Second paragraph.</p></div>"
|
||||
soup = BeautifulSoup('<div><p>First paragraph.</p><p>Second paragraph.</p></div>', 'html.parser')
|
||||
element = soup.find('div')
|
||||
|
||||
result = div_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, list)
|
||||
# Should match original test expectations
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertIsInstance(result[0], Paragraph)
|
||||
self.assertIsInstance(result[1], Paragraph)
|
||||
|
||||
|
||||
class TestStyledContentHandling(unittest.TestCase):
|
||||
"""Test styled content handling using patterns from existing tests."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.base_context = create_base_context()
|
||||
|
||||
def test_paragraph_with_bold_content(self):
|
||||
"""Test paragraph with bold content - adapted from test_bold_text."""
|
||||
# From: "<p>This is <strong>bold text</strong> in a paragraph.</p>"
|
||||
soup = BeautifulSoup('<p>This is <strong>bold text</strong> in a paragraph.</p>', 'html.parser')
|
||||
element = soup.find('p')
|
||||
|
||||
result = paragraph_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, Paragraph)
|
||||
words = list(result.words())
|
||||
self.assertEqual(len(words), 7) # From original test expectation
|
||||
|
||||
# Check that 'bold' and 'text' words have bold font weight (from original test)
|
||||
bold_word = words[2][1] # 'bold'
|
||||
text_word = words[3][1] # 'text'
|
||||
self.assertEqual(bold_word.text, "bold")
|
||||
self.assertEqual(bold_word.style.weight, FontWeight.BOLD)
|
||||
self.assertEqual(text_word.text, "text")
|
||||
self.assertEqual(text_word.style.weight, FontWeight.BOLD)
|
||||
|
||||
# Check that other words are not bold (from original test)
|
||||
normal_word = words[0][1] # 'This'
|
||||
self.assertEqual(normal_word.text, "This")
|
||||
self.assertNotEqual(normal_word.style.weight, FontWeight.BOLD)
|
||||
|
||||
def test_paragraph_with_mixed_formatting(self):
|
||||
"""Test mixed formatting - adapted from test_mixed_formatting."""
|
||||
# From: "<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style=\"color: blue;\">blue</span>..."
|
||||
soup = BeautifulSoup('<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, <span style="color: blue;">blue</span> text.</p>', 'html.parser')
|
||||
element = soup.find('p')
|
||||
|
||||
result = paragraph_handler(element, self.base_context)
|
||||
|
||||
self.assertIsInstance(result, Paragraph)
|
||||
words = list(result.words())
|
||||
|
||||
# Check for bold word (from original test pattern)
|
||||
bold_words = [w for _, w in words if w.style.weight == FontWeight.BOLD]
|
||||
self.assertGreater(len(bold_words), 0, "Should have bold words")
|
||||
|
||||
# Check for italic word (from original test pattern)
|
||||
italic_words = [w for _, w in words if w.style.style == FontStyle.ITALIC]
|
||||
self.assertGreater(len(italic_words), 0, "Should have italic words")
|
||||
|
||||
# Check for blue colored word (from original test pattern)
|
||||
blue_words = [w for _, w in words if w.style.colour == (0, 0, 255)]
|
||||
self.assertGreater(len(blue_words), 0, "Should have blue colored words")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Test module for loading HTML files using the html_extraction module.
|
||||
|
||||
This test verifies that HTML files can be loaded from disk and processed
|
||||
using the html_extraction.parse_html_string function.
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
||||
from pyWebLayout.abstract.block import Block
|
||||
from pyWebLayout.style import Font
|
||||
|
||||
|
||||
class TestHTMLFileLoader(unittest.TestCase):
|
||||
"""Test class for HTML file loading functionality."""
|
||||
|
||||
def test_load_html_file(self):
|
||||
"""Test loading and parsing an HTML file from disk."""
|
||||
# Path to the test HTML file
|
||||
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
# Verify the test file exists
|
||||
self.assertTrue(os.path.exists(html_file_path), f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
# Read the HTML file
|
||||
with open(html_file_path, 'r', encoding='utf-8') as file:
|
||||
html_content = file.read()
|
||||
|
||||
# Verify we got some content
|
||||
self.assertGreater(len(html_content), 0, "HTML file should not be empty")
|
||||
|
||||
# Parse the HTML content using the html_extraction module
|
||||
try:
|
||||
blocks = parse_html_string(html_content)
|
||||
except Exception as e:
|
||||
self.fail(f"Failed to parse HTML file: {e}")
|
||||
|
||||
# Verify we got some blocks
|
||||
self.assertIsInstance(blocks, list, "parse_html_string should return a list")
|
||||
self.assertGreater(len(blocks), 0, "Should extract at least one block from the HTML file")
|
||||
|
||||
# Verify all returned items are Block instances
|
||||
for i, block in enumerate(blocks):
|
||||
self.assertIsInstance(block, Block, f"Item {i} should be a Block instance, got {type(block)}")
|
||||
|
||||
print(f"Successfully loaded and parsed HTML file with {len(blocks)} blocks")
|
||||
|
||||
def test_load_html_file_with_custom_font(self):
|
||||
"""Test loading HTML file with a custom base font."""
|
||||
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
# Skip if file doesn't exist
|
||||
if not os.path.exists(html_file_path):
|
||||
self.skipTest(f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
# Create a custom font
|
||||
custom_font = Font(font_size=14, colour=(100, 100, 100))
|
||||
|
||||
# Read and parse with custom font
|
||||
with open(html_file_path, 'r', encoding='utf-8') as file:
|
||||
html_content = file.read()
|
||||
|
||||
blocks = parse_html_string(html_content, base_font=custom_font)
|
||||
|
||||
# Verify we got blocks
|
||||
self.assertGreater(len(blocks), 0, "Should extract blocks with custom font")
|
||||
|
||||
print(f"Successfully parsed HTML file with custom font, got {len(blocks)} blocks")
|
||||
|
||||
def test_load_html_file_content_types(self):
|
||||
"""Test that the loaded HTML file contains expected content types."""
|
||||
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
# Skip if file doesn't exist
|
||||
if not os.path.exists(html_file_path):
|
||||
self.skipTest(f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
with open(html_file_path, 'r', encoding='utf-8') as file:
|
||||
html_content = file.read()
|
||||
|
||||
blocks = parse_html_string(html_content)
|
||||
|
||||
# Check that we have different types of blocks
|
||||
block_type_names = [type(block).__name__ for block in blocks]
|
||||
unique_types = set(block_type_names)
|
||||
|
||||
# A Wikipedia page should contain multiple types of content
|
||||
self.assertGreater(len(unique_types), 1, "Should have multiple types of blocks in Wikipedia page")
|
||||
|
||||
print(f"Found block types: {sorted(unique_types)}")
|
||||
|
||||
def test_html_file_size_handling(self):
|
||||
"""Test that large HTML files can be handled gracefully."""
|
||||
html_file_path = os.path.join("tests", "data", "Kimi Räikkönen - Wikipedia.html")
|
||||
|
||||
# Skip if file doesn't exist
|
||||
if not os.path.exists(html_file_path):
|
||||
self.skipTest(f"Test HTML file not found: {html_file_path}")
|
||||
|
||||
# Get file size
|
||||
file_size = os.path.getsize(html_file_path)
|
||||
print(f"HTML file size: {file_size} bytes")
|
||||
|
||||
# Read and parse
|
||||
with open(html_file_path, 'r', encoding='utf-8') as file:
|
||||
html_content = file.read()
|
||||
|
||||
# This should not raise an exception even for large files
|
||||
blocks = parse_html_string(html_content)
|
||||
|
||||
# Basic verification
|
||||
self.assertIsInstance(blocks, list)
|
||||
print(f"Successfully processed {file_size} byte file into {len(blocks)} blocks")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user