auto flake and corrections

This commit is contained in:
2025-11-08 23:46:15 +01:00
parent 1ea870eef5
commit 781a9b6c08
81 changed files with 4646 additions and 3718 deletions
+204 -167
View File
@@ -9,7 +9,6 @@ import unittest
import tempfile
import os
import shutil
from datetime import datetime
# Import ebooklib for creating test EPUB files
try:
@@ -21,8 +20,8 @@ except ImportError:
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
Paragraph, Heading, Quote, CodeBlock, HList,
ListStyle, Table, Image
)
from pyWebLayout.style import FontWeight, FontStyle, TextDecoration
@@ -30,12 +29,12 @@ 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
@@ -44,21 +43,21 @@ class TestEPUBReader(unittest.TestCase):
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',
@@ -75,31 +74,32 @@ class TestEPUBReader(unittest.TestCase):
</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_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')
@@ -109,7 +109,7 @@ class TestEPUBReader(unittest.TestCase):
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',
@@ -126,7 +126,7 @@ class TestEPUBReader(unittest.TestCase):
</body>
</html>
'''
# Chapter 2: Styled content
chapter2 = epub.EpubHtml(
title='Styled Content',
@@ -138,7 +138,7 @@ class TestEPUBReader(unittest.TestCase):
<head><title>Styled Content</title></head>
<body>
<h1>Styled Content</h1>
<p>This chapter contains various <strong>bold text</strong>, <em>italic text</em>,
<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>
@@ -147,7 +147,7 @@ class TestEPUBReader(unittest.TestCase):
</body>
</html>
'''
# Chapter 3: Lists and quotes
chapter3 = epub.EpubHtml(
title='Lists and Quotes',
@@ -159,30 +159,30 @@ class TestEPUBReader(unittest.TestCase):
<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>
<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',
@@ -194,7 +194,7 @@ class TestEPUBReader(unittest.TestCase):
<head><title>Tables and Code</title></head>
<body>
<h1>Tables and Code</h1>
<h2>Simple Table</h2>
<table>
<thead>
@@ -214,25 +214,25 @@ class TestEPUBReader(unittest.TestCase):
</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"),
@@ -240,31 +240,32 @@ class TestEPUBReader(unittest.TestCase):
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_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',
@@ -276,17 +277,17 @@ class TestEPUBReader(unittest.TestCase):
<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>
@@ -297,268 +298,284 @@ class TestEPUBReader(unittest.TestCase):
</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_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")
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")
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_iter())
for _, word in words:
if (word.style.weight == FontWeight.BOLD or
if (word.style.weight == FontWeight.BOLD or
word.style.style == FontStyle.ITALIC or
word.style.colour != (0, 0, 0)): # Non-black color
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")
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")
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(
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")
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(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',
'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',
@@ -574,22 +591,23 @@ class TestEPUBReader(unittest.TestCase):
</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_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)
@@ -597,12 +615,12 @@ class TestEPUBReader(unittest.TestCase):
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:
@@ -610,10 +628,10 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
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."""
@@ -623,7 +641,7 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
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',
@@ -635,22 +653,22 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
<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>,
<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>
@@ -662,19 +680,19 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
<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>,
@@ -684,26 +702,26 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
</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")
@@ -712,29 +730,31 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
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_iter())
for _, word in words:
if (word.style.weight == FontWeight.BOLD or
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)):
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")
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):
@@ -746,12 +766,11 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
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()
@@ -759,20 +778,31 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
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'
img_data = (
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00'
b'\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t'
b'\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a'
b'\x1f\x1e\x1d\x1a\x1c\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'9=82<.342'
b'\xff\xc0\x00\x11\x08\x00d\x00d\x01\x01\x11\x00\x02\x11\x01\x03'
b'\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x10\x01'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00'
b'\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',
@@ -788,36 +818,43 @@ class TestEPUBIntegrationWithHTMLExtraction(unittest.TestCase):
<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_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(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")
+129 -104
View File
@@ -14,14 +14,15 @@ 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_iter(), "This is a paragraph.".split(" ")):
for w1, t1 in zip(paragraphs[0].words_iter(),
"This is a paragraph.".split(" ")):
self.assertEqual(w1[1].text, t1)
def test_multiple(self):
@@ -31,26 +32,28 @@ class TestHTMLParagraph(unittest.TestCase):
self.assertEqual(len(paragraphs[0]), 4)
self.assertEqual(len(paragraphs[1]), 4)
for w1, t1 in zip(paragraphs[0].words_iter(), "This is a paragraph.".split(" ")):
for w1, t1 in zip(paragraphs[0].words_iter(),
"This is a paragraph.".split(" ")):
self.assertEqual(w1[1].text, t1)
for w1, t1 in zip(paragraphs[1].words_iter(), "This is another paragraph.".split(" ")):
for w1, t1 in zip(paragraphs[1].words_iter(),
"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_iter())
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'
@@ -58,7 +61,7 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
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")
@@ -70,9 +73,9 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
blocks = parse_html_string(text)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Paragraph)
words = list(blocks[0].words_iter())
# Check that 'italic' and 'text' words have italic font style
italic_word = words[2][1] # 'italic'
text_word = words[3][1] # 'text'
@@ -86,7 +89,7 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
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_iter())
underlined_word = words[2][1] # 'underlined'
self.assertEqual(underlined_word.style.decoration, TextDecoration.UNDERLINE)
@@ -96,50 +99,60 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
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_iter())
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>'
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_iter())
# 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")
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>"
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_iter())
# 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")
@@ -149,26 +162,29 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
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_iter())
# 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")
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_iter())
# 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")
@@ -176,7 +192,7 @@ class TestHTMLStyledParagraphs(unittest.TestCase):
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>"
@@ -195,17 +211,20 @@ class TestHTMLBlockElements(unittest.TestCase):
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>"
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]
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_iter())
self.assertEqual(len(words), 2) # "Heading" and number
self.assertEqual(words[0][1].text, "Heading")
@@ -216,7 +235,7 @@ class TestHTMLBlockElements(unittest.TestCase):
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)
@@ -228,7 +247,7 @@ class TestHTMLBlockElements(unittest.TestCase):
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)
@@ -239,7 +258,7 @@ class TestHTMLBlockElements(unittest.TestCase):
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)
@@ -257,10 +276,10 @@ class TestHTMLBlockElements(unittest.TestCase):
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:
@@ -306,12 +325,12 @@ class TestHTMLBlockElements(unittest.TestCase):
"""
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")
@@ -322,7 +341,7 @@ class TestHTMLBlockElements(unittest.TestCase):
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)
@@ -332,7 +351,7 @@ class TestHTMLBlockElements(unittest.TestCase):
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 = """
@@ -350,7 +369,7 @@ class TestHTMLComplexStructures(unittest.TestCase):
"""
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)
@@ -383,12 +402,12 @@ class TestHTMLComplexStructures(unittest.TestCase):
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 = """
@@ -397,21 +416,21 @@ class TestHTMLFontRegistryIntegration(unittest.TestCase):
<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")
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 = """
@@ -420,23 +439,23 @@ class TestHTMLFontRegistryIntegration(unittest.TestCase):
<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")
"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")
"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
@@ -449,65 +468,71 @@ class TestHTMLFontRegistryIntegration(unittest.TestCase):
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_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
<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_iter())
# Find words with different styles
normal_words = [w for _, w in words if w.style.weight == FontWeight.NORMAL
and w.style.style == FontStyle.NORMAL]
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")
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")
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 = """
@@ -518,46 +543,46 @@ class TestHTMLFontRegistryIntegration(unittest.TestCase):
<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")
"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_iter())
# 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")
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")
"Should create separate styles for style combinations")
if __name__ == '__main__':
+153 -122
View File
@@ -6,7 +6,7 @@ reusing test patterns from test_html_extraction.py that are known to pass.
"""
import unittest
from bs4 import BeautifulSoup, Tag
from bs4 import BeautifulSoup
from pyWebLayout.io.readers.html_extraction import (
create_base_context,
apply_element_styling,
@@ -50,11 +50,11 @@ 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)
@@ -62,113 +62,119 @@ class TestUtilityFunctions(unittest.TestCase):
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
# 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)]
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}")
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')
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)
@@ -176,170 +182,185 @@ class TestUtilityFunctions(unittest.TestCase):
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')
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')
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")
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_iter())
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]
# 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_iter())
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')
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')
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')
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')
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
@@ -356,76 +377,80 @@ class TestHandlerFunctions(unittest.TestCase):
</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')
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')
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)
@@ -435,23 +460,25 @@ class TestHandlerFunctions(unittest.TestCase):
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')
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_iter())
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'
@@ -459,31 +486,35 @@ class TestStyledContentHandling(unittest.TestCase):
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')
# From: "<p>This paragraph contains <strong>bold</strong>, <em>italic</em>..."
html_str = (
'<p>This paragraph contains <strong>bold</strong>, <em>italic</em>, '
'<span style="color: blue;">blue</span> text.</p>'
)
soup = BeautifulSoup(html_str, 'html.parser')
element = soup.find('p')
result = paragraph_handler(element, self.base_context)
self.assertIsInstance(result, Paragraph)
words = list(result.words_iter())
# 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")
+51 -33
View File
@@ -18,97 +18,115 @@ class TestHTMLFileLoader(unittest.TestCase):
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")
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}")
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")
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)}")
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")
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")
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")
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")
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")
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")
+3 -2
View File
@@ -112,13 +112,14 @@ class TestHTMLLinkEndToEnd(unittest.TestCase):
rendered = page.render()
self.assertIsNotNone(rendered)
print(f"\nSettings overlay test:")
print("\nSettings overlay test:")
print(f" Found {len(all_linked_words)} linked words")
print(f" Actions: {actions}")
print(f" Rendered: {rendered.size}")
# The links are successfully created and rendered!
# In a real application, query_point would be used to detect clicks on these links
# In a real application, query_point would be used to detect clicks on
# these links
def test_link_metadata_preserved(self):
"""Test that link metadata (title, type) is preserved through the workflow."""
+14 -8
View File
@@ -155,8 +155,11 @@ class TestHTMLLinkInteractivity(unittest.TestCase):
html = '''
<div>
<h2 style="text-align: center; font-size: 18px; font-weight: bold; margin: 10px 0;">Settings</h2>
<p style="padding: 15px; margin: 5px 0; background-color: #dc3545; text-align: center; border-radius: 5px;">
<a href="action:back_to_library" style="text-decoration: none; color: white; font-weight: bold; font-size: 14px;">◄ Back to Library</a>
<p style="padding: 15px; margin: 5px 0; background-color: #dc3545; text-align: center;
border-radius: 5px;">
<a href="action:back_to_library"
style="text-decoration: none; color: white; font-weight: bold; font-size: 14px;">
◄ Back to Library</a>
</p>
<p style="padding: 10px; margin: 5px 0; background-color: #f8f9fa; border-radius: 5px;">
<span style="font-weight: bold;">Font Size: 100%</span><br>
@@ -177,23 +180,26 @@ class TestHTMLLinkInteractivity(unittest.TestCase):
all_linked_words.append(word)
# Verify we found the expected links
self.assertGreater(len(all_linked_words), 0, "Should find LinkedWords in settings HTML")
self.assertGreater(
len(all_linked_words),
0,
"Should find LinkedWords in settings HTML")
# Check for specific link targets
link_targets = {word.location for word in all_linked_words}
self.assertIn("action:back_to_library", link_targets,
"Should find 'Back to Library' link")
"Should find 'Back to Library' link")
self.assertIn("setting:font_decrease", link_targets,
"Should find font decrease link")
"Should find font decrease link")
self.assertIn("setting:font_increase", link_targets,
"Should find font increase link")
"Should find font increase link")
# Verify the link texts
back_to_library_words = [w for w in all_linked_words
if w.location == "action:back_to_library"]
if w.location == "action:back_to_library"]
self.assertGreater(len(back_to_library_words), 0,
"Should have words linked to back_to_library action")
"Should have words linked to back_to_library action")
# Print debug info
print(f"\nFound {len(all_linked_words)} linked words:")
+42 -43
View File
@@ -7,173 +7,172 @@ from bs4 import BeautifulSoup
from pyWebLayout.io.readers.html_extraction import (
parse_html_string,
extract_text_content,
create_base_context,
apply_element_styling
create_base_context
)
from pyWebLayout.abstract.inline import LinkedWord
from pyWebLayout.abstract.functional import LinkType
from pyWebLayout.abstract.block import Paragraph
from pyWebLayout.style import Font
class TestHTMLLinkExtraction(unittest.TestCase):
"""Test cases for HTML hyperlink extraction."""
def setUp(self):
"""Set up test fixtures."""
self.base_context = create_base_context()
def test_simple_external_link(self):
"""Test extracting a simple external link."""
html = '<p>Visit <a href="https://example.com">this site</a> for more.</p>'
blocks = parse_html_string(html)
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], Paragraph)
paragraph = blocks[0]
words = list(paragraph.words)
# Should have: "Visit", "this", "site", "for", "more."
self.assertEqual(len(words), 5)
# Check that "this" and "site" are LinkedWords
self.assertIsInstance(words[1], LinkedWord)
self.assertIsInstance(words[2], LinkedWord)
# Check link properties
self.assertEqual(words[1].location, "https://example.com")
self.assertEqual(words[1].link_type, LinkType.EXTERNAL)
self.assertEqual(words[2].location, "https://example.com")
self.assertEqual(words[2].link_type, LinkType.EXTERNAL)
def test_internal_link(self):
"""Test extracting an internal anchor link."""
html = '<p>Go to <a href="#section2">section 2</a> below.</p>'
blocks = parse_html_string(html)
paragraph = blocks[0]
words = list(paragraph.words)
# Find LinkedWords
linked_words = [w for w in words if isinstance(w, LinkedWord)]
self.assertEqual(len(linked_words), 2) # "section" and "2"
# Check they're internal links
for word in linked_words:
self.assertEqual(word.link_type, LinkType.INTERNAL)
self.assertEqual(word.location, "#section2")
def test_multi_word_link(self):
"""Test that multi-word links create separate LinkedWords."""
html = '<p><a href="/next">click here for next page</a></p>'
blocks = parse_html_string(html)
paragraph = blocks[0]
words = list(paragraph.words)
# All words should be LinkedWords
self.assertEqual(len(words), 5)
for word in words:
self.assertIsInstance(word, LinkedWord)
self.assertEqual(word.location, "/next")
self.assertEqual(word.link_type, LinkType.INTERNAL)
def test_link_with_title(self):
"""Test extracting link with title attribute."""
html = '<p><a href="https://example.com" title="Visit Example">click</a></p>'
blocks = parse_html_string(html)
paragraph = blocks[0]
words = list(paragraph.words)
self.assertEqual(len(words), 1)
self.assertIsInstance(words[0], LinkedWord)
self.assertEqual(words[0].link_title, "Visit Example")
def test_mixed_linked_and_normal_text(self):
"""Test paragraph with both linked and normal text."""
html = '<p>Some <a href="/page">linked text</a> and normal text.</p>'
blocks = parse_html_string(html)
paragraph = blocks[0]
words = list(paragraph.words)
# "Some" - normal
# "linked" - LinkedWord
# "text" - LinkedWord
# "text" - LinkedWord
# "and" - normal
# "normal" - normal
# "text." - normal
self.assertNotIsInstance(words[0], LinkedWord) # "Some"
self.assertIsInstance(words[1], LinkedWord) # "linked"
self.assertIsInstance(words[2], LinkedWord) # "text"
self.assertNotIsInstance(words[3], LinkedWord) # "and"
def test_link_without_href(self):
"""Test that <a> without href is treated as normal text."""
html = '<p><a>not a link</a></p>'
blocks = parse_html_string(html)
paragraph = blocks[0]
words = list(paragraph.words)
# Should be regular Words, not LinkedWords
for word in words:
self.assertNotIsInstance(word, LinkedWord)
def test_javascript_link(self):
"""Test that javascript: links are detected as API type."""
html = '<p><a href="javascript:alert()">click</a></p>'
blocks = parse_html_string(html)
paragraph = blocks[0]
words = list(paragraph.words)
self.assertIsInstance(words[0], LinkedWord)
self.assertEqual(words[0].link_type, LinkType.API)
def test_nested_formatting_in_link(self):
"""Test link with nested formatting."""
html = '<p><a href="/page">text with <strong>bold</strong> word</a></p>'
blocks = parse_html_string(html)
paragraph = blocks[0]
words = list(paragraph.words)
# All should be LinkedWords regardless of formatting
for word in words:
self.assertIsInstance(word, LinkedWord)
self.assertEqual(word.location, "/page")
def test_multiple_links_in_paragraph(self):
"""Test paragraph with multiple separate links."""
html = '<p><a href="/page1">first</a> and <a href="/page2">second</a> link</p>'
blocks = parse_html_string(html)
paragraph = blocks[0]
words = list(paragraph.words)
# Find LinkedWords and their locations
linked_words = [(w.text, w.location) for w in words if isinstance(w, LinkedWord)]
linked_words = [(w.text, w.location)
for w in words if isinstance(w, LinkedWord)]
# Should have "first" linked to /page1 and "second" linked to /page2
self.assertIn(("first", "/page1"), linked_words)
self.assertIn(("second", "/page2"), linked_words)
def test_extract_text_content_with_links(self):
"""Test extract_text_content directly with link elements."""
html = '<span>Visit <a href="https://example.com">our site</a> today</span>'
soup = BeautifulSoup(html, 'html.parser')
element = soup.find('span')
context = create_base_context()
words = extract_text_content(element, context)
# Should have: "Visit", "our", "site", "today"
self.assertEqual(len(words), 4)
# Check types
self.assertNotIsInstance(words[0], LinkedWord) # "Visit"
self.assertIsInstance(words[1], LinkedWord) # "our"