This commit is contained in:
2025-06-08 17:17:09 +02:00
parent a014de854e
commit f6ae60217f
19 changed files with 2777 additions and 944 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Demonstration of the refactored alignment handler system.
This shows how the nested alignment logic has been replaced with a clean handler pattern.
"""
from pyWebLayout.concrete.text import (
Line, Text,
LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
)
from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Font
def demonstrate_handler_system():
"""Demonstrate the new alignment handler system."""
print("=" * 60)
print("ALIGNMENT HANDLER SYSTEM DEMONSTRATION")
print("=" * 60)
print("\n1. HANDLER CREATION:")
print(" The system now uses three specialized handlers:")
# Create handlers
left_handler = LeftAlignmentHandler()
center_handler = CenterRightAlignmentHandler(Alignment.CENTER)
right_handler = CenterRightAlignmentHandler(Alignment.RIGHT)
justify_handler = JustifyAlignmentHandler()
print(f" • LeftAlignmentHandler: {type(left_handler).__name__}")
print(f" • CenterRightAlignmentHandler (Center): {type(center_handler).__name__}")
print(f" • CenterRightAlignmentHandler (Right): {type(right_handler).__name__}")
print(f" • JustifyAlignmentHandler: {type(justify_handler).__name__}")
print("\n2. AUTOMATIC HANDLER SELECTION:")
print(" Lines automatically choose the correct handler based on alignment:")
font = Font()
line_size = (300, 30)
spacing = (5, 20)
alignments = [
(Alignment.LEFT, "Left"),
(Alignment.CENTER, "Center"),
(Alignment.RIGHT, "Right"),
(Alignment.JUSTIFY, "Justify")
]
for alignment, name in alignments:
line = Line(spacing, (0, 0), line_size, font, halign=alignment)
handler_type = type(line._alignment_handler).__name__
print(f"{name:7}{handler_type}")
print("\n3. HYPHENATION INTEGRATION:")
print(" Each handler has its own hyphenation strategy:")
# Sample text objects and test conditions
sample_text = [Text("Hello", font), Text("World", font)]
word_width = 80
available_width = 70 # Word doesn't fit
min_spacing = 5
handlers = [
("Left", left_handler),
("Center", center_handler),
("Right", right_handler),
("Justify", justify_handler)
]
for name, handler in handlers:
should_hyphenate = handler.should_try_hyphenation(
sample_text, word_width, available_width, min_spacing)
print(f"{name:7}: should_hyphenate = {should_hyphenate}")
print("\n4. SPACING CALCULATIONS:")
print(" Each handler calculates spacing and positioning differently:")
for name, handler in handlers:
spacing_calc, x_position = handler.calculate_spacing_and_position(
sample_text, 300, 5, 20)
print(f"{name:7}: spacing={spacing_calc:2d}, position={x_position:3d}")
print("\n5. WORD ADDITION WITH INTELLIGENT HYPHENATION:")
print(" The system now tries different hyphenation options for optimal spacing:")
# Test with a word that might benefit from hyphenation
test_line = Line(spacing, (0, 0), (200, 30), font, halign=Alignment.JUSTIFY)
test_words = ["This", "is", "a", "demonstration", "of", "smart", "hyphenation"]
for word in test_words:
result = test_line.add_word(word)
if result:
print(f" • Word '{word}' → remainder: '{result}' (line full)")
break
else:
print(f" • Added '{word}' successfully")
print(f" • Final line contains {len(test_line.text_objects)} text objects")
print("\n6. BENEFITS OF THE NEW SYSTEM:")
print(" ✓ Separation of concerns - each alignment has its own handler")
print(" ✓ Extensible - easy to add new alignment types")
print(" ✓ Intelligent hyphenation - considers spacing quality")
print(" ✓ Clean code - no more nested if/else alignment logic")
print(" ✓ Testable - each handler can be tested independently")
print("\n" + "=" * 60)
print("REFACTORING COMPLETE - ALIGNMENT HANDLERS WORKING!")
print("=" * 60)
if __name__ == "__main__":
demonstrate_handler_system()
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
Demo script showing the viewport system in action.
This demonstrates how the viewport provides a movable window into large content,
enabling efficient scrolling without rendering the entire content at once.
"""
import os
from PIL import Image
from pyWebLayout.concrete import (
Viewport, ScrollablePageContent, Text, Box, RenderableImage
)
from pyWebLayout.style.fonts import Font, FontWeight
from pyWebLayout.style.layout import Alignment
def create_large_document_content():
"""Create a large document to demonstrate viewport scrolling"""
# Create scrollable content container
content = ScrollablePageContent(content_width=800, initial_height=100)
# Add a title
title_font = Font(font_size=24, weight=FontWeight.BOLD)
title = Text("Large Document Demo", title_font)
content.add_child(title)
# Add spacing
content.add_child(Box((0, 0), (1, 20)))
# Add many paragraphs to create a long document
paragraph_font = Font(font_size=14)
for i in range(50):
# Section header
section_font = Font(font_size=18, weight=FontWeight.BOLD)
header = Text(f"Section {i+1}", section_font)
content.add_child(header)
# Add some spacing
content.add_child(Box((0, 0), (1, 10)))
# Add paragraph content
paragraphs = [
f"This is paragraph {i+1}, line 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
f"This is paragraph {i+1}, line 2. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
f"This is paragraph {i+1}, line 3. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
f"This is paragraph {i+1}, line 4. Duis aute irure dolor in reprehenderit in voluptate velit esse.",
f"This is paragraph {i+1}, line 5. Excepteur sint occaecat cupidatat non proident, sunt in culpa."
]
for para_text in paragraphs:
para = Text(para_text, paragraph_font)
content.add_child(para)
content.add_child(Box((0, 0), (1, 5))) # Line spacing
# Add section spacing
content.add_child(Box((0, 0), (1, 15)))
return content
def demo_viewport_rendering():
"""Demonstrate viewport rendering at different scroll positions"""
print("Creating large document content...")
content = create_large_document_content()
print(f"Content size: {content.get_content_height()} pixels tall")
# Create viewport
viewport = Viewport(viewport_size=(800, 600), background_color=(255, 255, 255))
# Add content to viewport
viewport.add_content(content)
print(f"Viewport content size: {viewport.content_size}")
print(f"Max scroll Y: {viewport.max_scroll_y}")
# Create output directory
os.makedirs("output/viewport_demo", exist_ok=True)
# Render viewport at different scroll positions
scroll_positions = [
(0, "top"),
(viewport.max_scroll_y // 4, "quarter"),
(viewport.max_scroll_y // 2, "middle"),
(viewport.max_scroll_y * 3 // 4, "three_quarters"),
(viewport.max_scroll_y, "bottom")
]
for scroll_y, label in scroll_positions:
print(f"Rendering viewport at scroll position {scroll_y} ({label})...")
# Scroll to position
viewport.scroll_to(0, scroll_y)
# Get scroll info
scroll_info = viewport.get_scroll_info()
print(f" Scroll progress: {scroll_info['scroll_progress_y']:.2%}")
print(f" Visible elements: {len(viewport.get_visible_elements())}")
# Render viewport
viewport_img = viewport.render()
# Save image
output_path = f"output/viewport_demo/viewport_{label}.png"
viewport_img.save(output_path)
print(f" Saved: {output_path}")
print("\nViewport demo complete!")
return viewport
def demo_hit_testing():
"""Demonstrate hit testing in the viewport"""
print("\nTesting hit detection...")
# Create simple content for hit testing
content = ScrollablePageContent(content_width=800, initial_height=100)
# Add clickable elements
for i in range(10):
text = Text(f"Clickable text item {i}", Font(font_size=16))
content.add_child(text)
content.add_child(Box((0, 0), (1, 20))) # Spacing
# Create viewport
viewport = Viewport(viewport_size=(800, 300))
viewport.add_content(content)
# Test hit detection at different scroll positions
test_points = [(100, 50), (200, 100), (300, 150)]
for scroll_y in [0, 100, 200]:
viewport.scroll_to(0, scroll_y)
print(f"\nAt scroll position {scroll_y}:")
for point in test_points:
hit_element = viewport.hit_test(point)
if hit_element:
element_text = getattr(hit_element, '_text', 'Unknown element')
print(f" Point {point}: Hit '{element_text}'")
else:
print(f" Point {point}: No element")
def demo_scroll_methods():
"""Demonstrate different scrolling methods"""
print("\nTesting scroll methods...")
# Create content
content = ScrollablePageContent(content_width=800, initial_height=100)
for i in range(20):
text = Text(f"Line {i+1}: This is some sample text for scrolling demo", Font(font_size=14))
content.add_child(text)
content.add_child(Box((0, 0), (1, 5)))
# Create viewport
viewport = Viewport(viewport_size=(800, 200))
viewport.add_content(content)
print(f"Content height: {viewport.content_size[1]}")
print(f"Viewport height: {viewport.viewport_size[1]}")
print(f"Max scroll Y: {viewport.max_scroll_y}")
# Test different scroll methods
print("\nTesting scroll methods:")
# Scroll by lines
print("Scrolling down 5 lines...")
for i in range(5):
viewport.scroll_line_down(20)
print(f" After line {i+1}: offset = {viewport.viewport_offset}")
# Scroll by pages
print("Scrolling down 1 page...")
viewport.scroll_page_down()
print(f" After page down: offset = {viewport.viewport_offset}")
# Scroll to bottom
print("Scrolling to bottom...")
viewport.scroll_to_bottom()
print(f" At bottom: offset = {viewport.viewport_offset}")
# Scroll to top
print("Scrolling to top...")
viewport.scroll_to_top()
print(f" At top: offset = {viewport.viewport_offset}")
def main():
"""Run all viewport demos"""
print("=== Viewport System Demo ===")
# Demo 1: Basic viewport rendering
viewport = demo_viewport_rendering()
# Demo 2: Hit testing
demo_hit_testing()
# Demo 3: Scroll methods
demo_scroll_methods()
print("\n=== Demo Complete ===")
print("Check the output/viewport_demo/ directory for rendered images.")
# Show final scroll info
scroll_info = viewport.get_scroll_info()
print(f"\nFinal viewport state:")
print(f" Content size: {scroll_info['content_size']}")
print(f" Viewport size: {scroll_info['viewport_size']}")
print(f" Current offset: {scroll_info['offset']}")
print(f" Scroll progress: {scroll_info['scroll_progress_y']:.1%}")
print(f" Can scroll up: {scroll_info['can_scroll_up']}")
print(f" Can scroll down: {scroll_info['can_scroll_down']}")
if __name__ == "__main__":
main()
+406
View File
@@ -0,0 +1,406 @@
#!/usr/bin/env python3
"""
Basic EPUB Reader with Pagination using pyWebLayout
This reader loads EPUB files and displays them with page-by-page navigation
using the pyWebLayout system. It follows the proper architecture where:
- EPUBReader loads EPUB files into Document/Chapter objects
- Page renders those abstract objects into visual pages
- The UI handles pagination and navigation
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
from typing import List, Optional
from PIL import Image, ImageTk
from pyWebLayout.io.readers.epub_reader import EPUBReader
from pyWebLayout.concrete.page import Page
from pyWebLayout.style.fonts import Font
from pyWebLayout.abstract.document import Document, Chapter, Book
from pyWebLayout.io.readers.html_extraction import parse_html_string
class EPUBReaderApp:
"""Main EPUB reader application using Tkinter"""
def __init__(self):
self.root = tk.Tk()
self.root.title("pyWebLayout EPUB Reader")
self.root.geometry("900x700")
# Application state
self.current_epub: Optional[EPUBReader] = None
self.current_document: Optional[Document] = None
self.rendered_pages: List[Page] = []
self.current_page_index = 0
# Page settings
self.page_width = 700
self.page_height = 550
self.blocks_per_page = 3 # Fewer blocks per page for better readability
self.setup_ui()
def setup_ui(self):
"""Setup the user interface"""
# Create main frame
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Top control frame
control_frame = ttk.Frame(main_frame)
control_frame.pack(fill=tk.X, pady=(0, 10))
# File operations
self.open_btn = ttk.Button(control_frame, text="Open EPUB", command=self.open_epub)
self.open_btn.pack(side=tk.LEFT, padx=(0, 10))
# Book info
self.book_info_label = ttk.Label(control_frame, text="No book loaded")
self.book_info_label.pack(side=tk.LEFT, expand=True)
# Navigation frame
nav_frame = ttk.Frame(main_frame)
nav_frame.pack(fill=tk.X, pady=(0, 10))
# Navigation buttons
self.prev_btn = ttk.Button(nav_frame, text="◀ Previous", command=self.previous_page, state=tk.DISABLED)
self.prev_btn.pack(side=tk.LEFT, padx=(0, 10))
self.next_btn = ttk.Button(nav_frame, text="Next ▶", command=self.next_page, state=tk.DISABLED)
self.next_btn.pack(side=tk.LEFT, padx=(0, 10))
# Page info
self.page_info_label = ttk.Label(nav_frame, text="Page 0 of 0")
self.page_info_label.pack(side=tk.LEFT, padx=(20, 0))
# Chapter selector
ttk.Label(nav_frame, text="Chapter:").pack(side=tk.LEFT, padx=(20, 5))
self.chapter_var = tk.StringVar()
self.chapter_combo = ttk.Combobox(nav_frame, textvariable=self.chapter_var, state="readonly", width=30)
self.chapter_combo.pack(side=tk.LEFT, padx=(0, 10))
self.chapter_combo.bind('<<ComboboxSelected>>', self.on_chapter_selected)
# Content frame with canvas
content_frame = ttk.Frame(main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
# Create canvas for page display
self.canvas = tk.Canvas(content_frame, bg='white', width=self.page_width, height=self.page_height)
self.canvas.pack(expand=True)
# Status bar
self.status_var = tk.StringVar(value="Ready - Open an EPUB file to begin")
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
status_bar.pack(fill=tk.X, pady=(10, 0))
# Bind keyboard shortcuts
self.root.bind('<Key-Left>', lambda e: self.previous_page())
self.root.bind('<Key-Right>', lambda e: self.next_page())
self.root.bind('<Key-space>', lambda e: self.next_page())
self.root.focus_set() # Allow keyboard input
def open_epub(self):
"""Open and load an EPUB file"""
file_path = filedialog.askopenfilename(
title="Open EPUB File",
filetypes=[("EPUB files", "*.epub"), ("All files", "*.*")]
)
if file_path:
self.load_epub(file_path)
def load_epub(self, file_path: str):
"""Load an EPUB file and prepare for display"""
try:
self.status_var.set("Loading EPUB file...")
self.root.update()
# Load the EPUB using the EPUBReader
self.current_epub = EPUBReader(file_path)
# Get the document structure from the EPUB
self.current_document = self.current_epub.read()
# Update book info
if isinstance(self.current_document, Book):
title = self.current_document.get_title() or "Unknown Title"
author = self.current_document.get_author() or "Unknown Author"
self.book_info_label.config(text=f"{title} by {author}")
else:
title = getattr(self.current_document, 'title', 'Unknown Title')
self.book_info_label.config(text=title)
# Populate chapter list
self.populate_chapter_list()
# Create pages from the document
self.create_pages_from_document()
# Show first page
self.current_page_index = 0
self.display_current_page()
self.update_navigation()
self.status_var.set(f"Loaded: {os.path.basename(file_path)} - {len(self.rendered_pages)} pages")
except Exception as e:
self.status_var.set(f"Error loading EPUB: {str(e)}")
messagebox.showerror("Error", f"Failed to load EPUB file:\n{str(e)}")
print(f"Detailed error: {e}")
import traceback
traceback.print_exc()
def populate_chapter_list(self):
"""Populate the chapter selection dropdown"""
if not self.current_document:
return
chapters = []
# Check if it's a Book with chapters
if isinstance(self.current_document, Book) and self.current_document.chapters:
for i, chapter in enumerate(self.current_document.chapters):
chapter_title = chapter.title or f"Chapter {i+1}"
chapters.append(chapter_title)
else:
# Fallback: add a single "Document" entry
chapters.append("Document")
self.chapter_combo['values'] = chapters
if chapters:
self.chapter_combo.set(chapters[0])
def create_pages_from_document(self):
"""Create pages using the new external pagination system with block handlers"""
if not self.current_document:
return
self.rendered_pages.clear()
try:
# Get all blocks from the document
all_blocks = []
if isinstance(self.current_document, Book) and self.current_document.chapters:
# Process chapters
for chapter in self.current_document.chapters:
all_blocks.extend(chapter.blocks)
else:
# Process document blocks directly
all_blocks = self.current_document.blocks
# If no blocks found, try to create some from EPUB content
if not all_blocks:
all_blocks = self.create_blocks_from_epub_content()
# Use the new external pagination system
remaining_blocks = all_blocks
while remaining_blocks:
# Create a new page
current_page = Page(size=(self.page_width, self.page_height))
# Fill the page using the external pagination system
next_index, remainder_blocks = current_page.fill_with_blocks(remaining_blocks)
# Add the page if it has content
if current_page._children:
self.rendered_pages.append(current_page)
# Update remaining blocks for next iteration
if remainder_blocks:
# We have remainder blocks (partial content)
remaining_blocks = remainder_blocks
elif next_index < len(remaining_blocks):
# We stopped at a specific index
remaining_blocks = remaining_blocks[next_index:]
else:
# All blocks processed
remaining_blocks = []
# Safety check to prevent infinite loops
if not current_page._children and remaining_blocks:
print(f"Warning: Could not fit any content on page, skipping {len(remaining_blocks)} blocks")
break
# If no pages were created, create a default one
if not self.rendered_pages:
self.create_default_page()
except Exception as e:
print(f"Error creating pages: {e}")
import traceback
traceback.print_exc()
self.create_default_page()
def create_blocks_from_epub_content(self):
"""Create blocks from raw EPUB content when document parsing fails"""
blocks = []
try:
# Get HTML content from EPUB spine items
spine_items = self.current_epub.spine[:3] # Limit to first 3 items
for item_id in spine_items:
try:
# Get the manifest item
if item_id in self.current_epub.manifest:
item = self.current_epub.manifest[item_id]
file_path = item['path']
# Read the HTML content
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Parse HTML content into blocks
html_blocks = parse_html_string(content)
blocks.extend(html_blocks[:5]) # Limit blocks per item
except Exception as e:
print(f"Error processing spine item {item_id}: {e}")
continue
except Exception as e:
print(f"Error getting EPUB content: {e}")
return blocks
def create_default_page(self):
"""Create a default page when content loading fails"""
page = Page(size=(self.page_width, self.page_height))
# Add some default content
from pyWebLayout.concrete.text import Text
default_font = Font()
if self.current_document:
title = getattr(self.current_document, 'title', None)
if title:
page.add_child(Text(f"Book: {title}", default_font))
page.add_child(Text("Content is loading...", default_font))
else:
page.add_child(Text("EPUB content loaded", default_font))
page.add_child(Text("Use arrow keys or buttons to navigate", default_font))
self.rendered_pages = [page]
def display_current_page(self):
"""Display the current page on the canvas"""
if not self.rendered_pages or self.current_page_index >= len(self.rendered_pages):
return
try:
# Clear the canvas
self.canvas.delete("all")
# Get the current page
page = self.rendered_pages[self.current_page_index]
# Render the page
page_image = page.render()
# Convert to PhotoImage
self.photo = ImageTk.PhotoImage(page_image)
# Calculate position to center the page
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width > 1 and canvas_height > 1: # Canvas is properly sized
x_pos = max(0, (canvas_width - page_image.width) // 2)
y_pos = max(0, (canvas_height - page_image.height) // 2)
else:
x_pos, y_pos = 0, 0
# Display the page
self.canvas.create_image(x_pos, y_pos, anchor=tk.NW, image=self.photo)
except Exception as e:
# Display error message
self.canvas.delete("all")
self.canvas.create_text(
self.page_width // 2, self.page_height // 2,
text=f"Error displaying page: {str(e)}",
fill="red", font=("Arial", 12)
)
print(f"Display error: {e}")
def previous_page(self):
"""Navigate to the previous page"""
if self.current_page_index > 0:
self.current_page_index -= 1
self.display_current_page()
self.update_navigation()
def next_page(self):
"""Navigate to the next page"""
if self.current_page_index < len(self.rendered_pages) - 1:
self.current_page_index += 1
self.display_current_page()
self.update_navigation()
def update_navigation(self):
"""Update navigation button states and page info"""
if not self.rendered_pages:
self.prev_btn.config(state=tk.DISABLED)
self.next_btn.config(state=tk.DISABLED)
self.page_info_label.config(text="Page 0 of 0")
return
# Update button states
self.prev_btn.config(state=tk.NORMAL if self.current_page_index > 0 else tk.DISABLED)
self.next_btn.config(state=tk.NORMAL if self.current_page_index < len(self.rendered_pages) - 1 else tk.DISABLED)
# Update page info
page_num = self.current_page_index + 1
total_pages = len(self.rendered_pages)
self.page_info_label.config(text=f"Page {page_num} of {total_pages}")
def on_chapter_selected(self, event=None):
"""Handle chapter selection"""
if not self.current_document or not self.rendered_pages:
return
selected_chapter = self.chapter_var.get()
# For now, just go to the first page
# In a more sophisticated implementation, we'd track chapter start pages
self.current_page_index = 0
self.display_current_page()
self.update_navigation()
self.status_var.set(f"Viewing: {selected_chapter}")
def run(self):
"""Start the EPUB reader application"""
# Make canvas responsive
def on_configure(event):
# Redisplay current page when canvas is resized
if hasattr(self, 'photo'):
self.root.after_idle(self.display_current_page)
self.canvas.bind('<Configure>', on_configure)
# Start the main loop
self.root.mainloop()
def main():
"""Main function to run the EPUB reader"""
print("Starting pyWebLayout EPUB Reader...")
try:
app = EPUBReaderApp()
app.run()
except Exception as e:
print(f"Error starting EPUB reader: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,771 @@
#!/usr/bin/env python3
"""
Enhanced HTML Browser using pyWebLayout with Viewport System
This browser uses the new viewport system to enable efficient scrolling
within HTML pages, only rendering the visible portion of large documents.
"""
import re
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
from PIL import Image, ImageTk, ImageDraw
from typing import Dict, List, Optional, Tuple, Any
import webbrowser
import os
from urllib.parse import urljoin, urlparse
import requests
from io import BytesIO
import pyperclip
# Import pyWebLayout components including the new viewport system
from pyWebLayout.concrete import (
Page, Container, Box, Text, RenderableImage,
RenderableLink, RenderableButton, RenderableForm, RenderableFormField,
Viewport, ScrollablePageContent
)
from pyWebLayout.abstract.functional import (
Link, Button, Form, FormField, LinkType, FormFieldType
)
from pyWebLayout.abstract.block import Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style.fonts import Font, FontWeight, FontStyle, TextDecoration
from pyWebLayout.style.layout import Alignment
from pyWebLayout.typesetting.paragraph_layout import ParagraphLayout, ParagraphLayoutResult
from pyWebLayout.io.readers.html_extraction import parse_html_string
class HTMLViewportAdapter:
"""Adapter to convert HTML to viewport using the proper HTML extraction system"""
def __init__(self):
pass
def parse_html_string(self, html_content: str, base_url: str = "", viewport_size: Tuple[int, int] = (800, 600)) -> Viewport:
"""Parse HTML string and return a Viewport object with scrollable content using the proper parser"""
# Use the proper HTML extraction system
base_font = Font(font_size=14)
blocks = parse_html_string(html_content, base_font)
# Extract title
title_match = re.search(r'<title>(.*?)</title>', html_content, re.IGNORECASE)
title = title_match.group(1) if title_match else "Untitled"
# Create scrollable content container
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
# Convert abstract blocks to renderable objects using Page's conversion system
page = Page(size=(viewport_size[0], 10000)) # Large temporary page
# Add blocks to page and let it handle the conversion
for i, block in enumerate(blocks):
renderable = page._convert_block_to_renderable(block)
if renderable:
content.add_child(renderable)
# Add spacing between blocks (but not after the last block)
if i < len(blocks) - 1:
content.add_child(Box((0, 0), (1, 8)))
# Create viewport and add the content
viewport = Viewport(viewport_size=viewport_size, background_color=(255, 255, 255))
viewport.add_content(content)
viewport.title = title
return viewport
def parse_html_file(self, file_path: str, viewport_size: Tuple[int, int] = (800, 600)) -> Viewport:
"""Parse HTML file and return a Viewport object"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
html_content = f.read()
base_url = os.path.dirname(os.path.abspath(file_path))
return self.parse_html_string(html_content, base_url, viewport_size)
except Exception as e:
# Create error viewport
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
error_text = Text(f"Error loading file: {str(e)}", Font(font_size=16, colour=(255, 0, 0)))
content.add_child(error_text)
viewport = Viewport(viewport_size=viewport_size, background_color=(255, 255, 255))
viewport.add_content(content)
viewport.title = "Error"
return viewport
class ViewportBrowserWindow:
"""Enhanced browser window using Tkinter with Viewport support"""
def __init__(self):
self.root = tk.Tk()
self.root.title("pyWebLayout HTML Browser with Viewport")
self.root.geometry("1000x800")
self.current_viewport = None
self.history = []
self.history_index = -1
# Scrolling parameters
self.scroll_speed = 20 # pixels per scroll
self.page_scroll_ratio = 0.9 # fraction of viewport height for page scroll
# Text selection variables
self.selection_start = None
self.selection_end = None
self.is_selecting = False
self.selected_text = ""
self.text_elements = []
self.selection_overlay = None
self.setup_ui()
def setup_ui(self):
"""Setup the user interface"""
# Create main frame
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Navigation frame
nav_frame = ttk.Frame(main_frame)
nav_frame.pack(fill=tk.X, pady=(0, 5))
# Navigation buttons
self.back_btn = ttk.Button(nav_frame, text="", command=self.go_back, state=tk.DISABLED)
self.back_btn.pack(side=tk.LEFT, padx=(0, 5))
self.forward_btn = ttk.Button(nav_frame, text="", command=self.go_forward, state=tk.DISABLED)
self.forward_btn.pack(side=tk.LEFT, padx=(0, 5))
self.refresh_btn = ttk.Button(nav_frame, text="", command=self.refresh)
self.refresh_btn.pack(side=tk.LEFT, padx=(0, 10))
# Address bar
ttk.Label(nav_frame, text="URL:").pack(side=tk.LEFT)
self.url_var = tk.StringVar()
self.url_entry = ttk.Entry(nav_frame, textvariable=self.url_var, width=50)
self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 5))
self.url_entry.bind('<Return>', self.navigate_to_url)
self.go_btn = ttk.Button(nav_frame, text="Go", command=self.navigate_to_url)
self.go_btn.pack(side=tk.LEFT, padx=(0, 10))
# File operations
self.open_btn = ttk.Button(nav_frame, text="Open File", command=self.open_file)
self.open_btn.pack(side=tk.LEFT)
# Content frame with scrollbars and viewport controls
content_frame = ttk.Frame(main_frame)
content_frame.pack(fill=tk.BOTH, expand=True)
# Create scrollbar frame
scroll_frame = ttk.Frame(content_frame)
scroll_frame.pack(side=tk.RIGHT, fill=tk.Y)
# Vertical scrollbar
self.v_scrollbar = ttk.Scrollbar(scroll_frame, orient=tk.VERTICAL, command=self.on_scrollbar)
self.v_scrollbar.pack(fill=tk.Y)
# Canvas for displaying viewport content
self.canvas = tk.Canvas(content_frame, bg='white')
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Scroll info frame
info_frame = ttk.Frame(main_frame)
info_frame.pack(fill=tk.X, pady=(5, 0))
self.scroll_info_var = tk.StringVar(value="")
ttk.Label(info_frame, textvariable=self.scroll_info_var).pack(side=tk.LEFT)
# Status bar
self.status_var = tk.StringVar(value="Ready")
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
status_bar.pack(fill=tk.X, pady=(5, 0))
# Bind events
self.canvas.bind('<Button-1>', self.on_click)
self.canvas.bind('<B1-Motion>', self.on_drag)
self.canvas.bind('<ButtonRelease-1>', self.on_release)
self.canvas.bind('<Motion>', self.on_mouse_move)
self.canvas.bind('<MouseWheel>', self.on_mouse_wheel) # Windows/Mac
self.canvas.bind('<Button-4>', self.on_mouse_wheel) # Linux scroll up
self.canvas.bind('<Button-5>', self.on_mouse_wheel) # Linux scroll down
# Keyboard shortcuts
self.root.bind('<Control-c>', self.copy_selection)
self.root.bind('<Control-a>', self.select_all)
self.root.bind('<Prior>', self.page_up) # Page Up
self.root.bind('<Next>', self.page_down) # Page Down
self.root.bind('<Home>', self.scroll_to_top) # Home
self.root.bind('<End>', self.scroll_to_bottom) # End
self.root.bind('<Up>', lambda e: self.scroll_by_lines(-1))
self.root.bind('<Down>', lambda e: self.scroll_by_lines(1))
# Context menu
self.setup_context_menu()
# Make canvas focusable
self.canvas.config(highlightthickness=1)
self.canvas.focus_set()
# Load default page
self.load_default_page()
def setup_context_menu(self):
"""Setup the right-click context menu"""
self.context_menu = tk.Menu(self.root, tearoff=0)
self.context_menu.add_command(label="Copy", command=self.copy_selection)
self.context_menu.add_command(label="Select All", command=self.select_all)
self.context_menu.add_separator()
self.context_menu.add_command(label="Scroll to Top", command=self.scroll_to_top)
self.context_menu.add_command(label="Scroll to Bottom", command=self.scroll_to_bottom)
# Bind right-click to show context menu
self.canvas.bind('<Button-3>', self.show_context_menu)
def show_context_menu(self, event):
"""Show context menu at mouse position"""
try:
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
def on_scrollbar(self, *args):
"""Handle scrollbar movement"""
if not self.current_viewport:
return
action = args[0]
if action == "moveto":
# Absolute position (0.0 to 1.0)
fraction = float(args[1])
max_scroll = self.current_viewport.max_scroll_y
new_y = int(fraction * max_scroll)
self.current_viewport.scroll_to(0, new_y)
self.update_viewport_display()
elif action in ["scroll", "step"]:
# Relative movement
direction = int(args[1])
# Handle the units parameter properly - it might be a string "units"
if len(args) > 2:
try:
units = int(args[2])
except ValueError:
# If args[2] is "units" string, default to 1
units = 1
else:
units = 1
if action == "scroll":
# Line-based scrolling
self.scroll_by_lines(direction * units)
elif action == "step":
# Page-based scrolling
self.scroll_by_pages(direction * units)
def update_scrollbar(self):
"""Update scrollbar position and size based on viewport state"""
if not self.current_viewport:
self.v_scrollbar.set(0, 1)
return
scroll_info = self.current_viewport.get_scroll_info()
content_height = scroll_info['content_size'][1]
viewport_height = scroll_info['viewport_size'][1]
current_offset = scroll_info['offset'][1]
if content_height <= viewport_height:
# No scrolling needed
self.v_scrollbar.set(0, 1)
else:
# Calculate scrollbar position and size
top_fraction = current_offset / content_height
bottom_fraction = (current_offset + viewport_height) / content_height
self.v_scrollbar.set(top_fraction, bottom_fraction)
# Update scroll info display
progress = scroll_info['scroll_progress_y']
self.scroll_info_var.set(f"Scroll: {progress:.1%} ({current_offset}/{content_height - viewport_height})")
def on_mouse_wheel(self, event):
"""Handle mouse wheel scrolling"""
if not self.current_viewport:
return
# Determine scroll direction and amount
if event.num == 4 or event.delta > 0:
# Scroll up
self.scroll_by_lines(-3)
elif event.num == 5 or event.delta < 0:
# Scroll down
self.scroll_by_lines(3)
def scroll_by_lines(self, lines: int):
"""Scroll by a number of lines"""
if not self.current_viewport:
return
delta_y = lines * self.scroll_speed
self.current_viewport.scroll_by(0, delta_y)
self.update_viewport_display()
def scroll_by_pages(self, pages: int):
"""Scroll by a number of pages"""
if not self.current_viewport:
return
viewport_height = self.current_viewport.viewport_size[1]
delta_y = int(pages * viewport_height * self.page_scroll_ratio)
self.current_viewport.scroll_by(0, delta_y)
self.update_viewport_display()
def page_up(self, event=None):
"""Scroll up by one page"""
self.scroll_by_pages(-1)
def page_down(self, event=None):
"""Scroll down by one page"""
self.scroll_by_pages(1)
def scroll_to_top(self, event=None):
"""Scroll to the top of the document"""
if not self.current_viewport:
return
self.current_viewport.scroll_to_top()
self.update_viewport_display()
def scroll_to_bottom(self, event=None):
"""Scroll to the bottom of the document"""
if not self.current_viewport:
return
self.current_viewport.scroll_to_bottom()
self.update_viewport_display()
def update_viewport_display(self):
"""Update the canvas display with current viewport content"""
if not self.current_viewport:
return
try:
# Render the current viewport
viewport_img = self.current_viewport.render()
# Convert to PhotoImage for Tkinter
self.photo = ImageTk.PhotoImage(viewport_img)
# Clear canvas and display image
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo)
# Update scrollbar
self.update_scrollbar()
except Exception as e:
self.status_var.set(f"Error rendering viewport: {str(e)}")
def on_drag(self, event):
"""Handle mouse dragging for text selection"""
canvas_x = self.canvas.canvasx(event.x)
canvas_y = self.canvas.canvasy(event.y)
if not self.is_selecting:
# Start selection
self.is_selecting = True
self.selection_start = (canvas_x, canvas_y)
self.selection_end = (canvas_x, canvas_y)
else:
# Update selection end
self.selection_end = (canvas_x, canvas_y)
# Update visual selection
self.update_selection_visual()
# Update status
self.status_var.set("Selecting text...")
def on_release(self, event):
"""Handle mouse release to complete text selection"""
if self.is_selecting:
canvas_x = self.canvas.canvasx(event.x)
canvas_y = self.canvas.canvasy(event.y)
self.selection_end = (canvas_x, canvas_y)
# Extract selected text
self.extract_selected_text()
# Update status
if self.selected_text:
self.status_var.set(f"Selected: {len(self.selected_text)} characters")
else:
self.status_var.set("No text selected")
self.clear_selection()
def update_selection_visual(self):
"""Update the visual representation of text selection"""
# Remove existing selection overlay
if self.selection_overlay:
self.canvas.delete(self.selection_overlay)
if self.selection_start and self.selection_end:
# Create selection rectangle
x1, y1 = self.selection_start
x2, y2 = self.selection_end
# Ensure proper coordinates (top-left to bottom-right)
left = min(x1, x2)
top = min(y1, y2)
right = max(x1, x2)
bottom = max(y1, y2)
# Draw selection rectangle with transparency effect
self.selection_overlay = self.canvas.create_rectangle(
left, top, right, bottom,
fill='blue', stipple='gray50', outline='blue', width=1
)
def extract_selected_text(self):
"""Extract text that falls within the selection area"""
if not self.selection_start or not self.selection_end or not self.current_viewport:
self.selected_text = ""
return
# Get selection bounds in viewport coordinates
x1, y1 = self.selection_start
x2, y2 = self.selection_end
left = min(x1, x2)
top = min(y1, y2)
right = max(x1, x2)
bottom = max(y1, y2)
# Convert to content coordinates
viewport_offset = self.current_viewport.viewport_offset
content_left = left + viewport_offset[0]
content_top = top + viewport_offset[1]
content_right = right + viewport_offset[0]
content_bottom = bottom + viewport_offset[1]
# Extract text elements in selection area
selected_elements = []
visible_elements = self.current_viewport.get_visible_elements()
for element, visible_origin, visible_size, clip_info in visible_elements:
# Check if element intersects with selection
elem_left = visible_origin[0]
elem_top = visible_origin[1]
elem_right = elem_left + visible_size[0]
elem_bottom = elem_top + visible_size[1]
if (elem_left < right and elem_right > left and
elem_top < bottom and elem_bottom > top):
# Extract text from element
if hasattr(element, '_text'):
text_content = element._text
if text_content.strip():
selected_elements.append((text_content.strip(), elem_left, elem_top))
# Sort by position (top to bottom, left to right)
selected_elements.sort(key=lambda x: (x[2], x[1]))
# Combine text
self.selected_text = " ".join([element[0] for element in selected_elements])
def copy_selection(self, event=None):
"""Copy selected text to clipboard"""
if self.selected_text:
try:
pyperclip.copy(self.selected_text)
self.status_var.set(f"Copied {len(self.selected_text)} characters to clipboard")
except Exception as e:
self.status_var.set(f"Error copying to clipboard: {str(e)}")
else:
self.status_var.set("No text selected to copy")
def select_all(self, event=None):
"""Select all text on the page"""
if not self.current_viewport:
return
# Set selection to entire viewport area
viewport_size = self.current_viewport.viewport_size
self.selection_start = (0, 0)
self.selection_end = viewport_size
self.is_selecting = True
# Extract all visible text
self.extract_selected_text()
# Update visual
self.update_selection_visual()
if self.selected_text:
self.status_var.set(f"Selected all visible text: {len(self.selected_text)} characters")
else:
self.status_var.set("No text found to select")
def clear_selection(self):
"""Clear the current text selection"""
self.selection_start = None
self.selection_end = None
self.is_selecting = False
self.selected_text = ""
# Remove visual selection
if self.selection_overlay:
self.canvas.delete(self.selection_overlay)
self.selection_overlay = None
self.status_var.set("Selection cleared")
def load_default_page(self):
"""Load a default welcome page"""
html_content = """
<html>
<head><title>pyWebLayout Browser with Viewport - Welcome</title></head>
<body>
<h1>Welcome to pyWebLayout Browser with Viewport System</h1>
<p>This enhanced browser uses the new viewport system for efficient scrolling through large documents.</p>
<h2>New Viewport Features:</h2>
<ul>
<li><b>Efficient Rendering:</b> Only visible content is rendered</li>
<li><b>Smooth Scrolling:</b> Mouse wheel, keyboard, and scrollbar support</li>
<li><b>Large Document Support:</b> Handle documents of any size</li>
<li><b>Memory Efficient:</b> Low memory usage even for huge pages</li>
</ul>
<h2>Scrolling Controls:</h2>
<p><b>Mouse Wheel:</b> Scroll up and down</p>
<p><b>Page Up/Down:</b> Scroll by viewport height</p>
<p><b>Home/End:</b> Jump to top/bottom</p>
<p><b>Arrow Keys:</b> Scroll line by line</p>
<p><b>Scrollbar:</b> Click and drag for precise positioning</p>
<h2>Text Selection:</h2>
<p>Click and drag to select text, then use <b>Ctrl+C</b> to copy</p>
<p>Use <b>Ctrl+A</b> to select all visible text</p>
<h3>Try scrolling with different methods!</h3>
<p>This page demonstrates the viewport system. All the content above and below is efficiently managed.</p>
<h2>Sample Content for Scrolling</h2>
<p>Here's some additional content to demonstrate scrolling capabilities:</p>
<h3>Lorem Ipsum</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
<h3>More Sample Text</h3>
<p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.</p>
<p>Totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p>
<h3>Even More Content</h3>
<p>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos.</p>
<p>Qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet.</p>
<p>Consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.</p>
<h2>Technical Details</h2>
<p>The viewport system works by:</p>
<ul>
<li>Creating a large content container that can hold any amount of content</li>
<li>Providing a viewport window that shows only a portion of the content</li>
<li>Efficiently calculating which elements are visible</li>
<li>Rendering only the visible elements</li>
<li>Supporting smooth scrolling through the content</li>
</ul>
<p>This allows handling of very large documents without performance issues.</p>
<h2>Load Your Own Content</h2>
<p>Use the "Open File" button to load local HTML files, or enter a URL in the address bar.</p>
</body>
</html>
"""
# Get current canvas size for viewport
self.root.update_idletasks()
canvas_width = max(800, self.canvas.winfo_width())
canvas_height = max(600, self.canvas.winfo_height())
parser = HTMLViewportAdapter()
self.current_viewport = parser.parse_html_string(html_content, viewport_size=(canvas_width, canvas_height))
# Update window title
if hasattr(self.current_viewport, 'title'):
self.root.title(f"pyWebLayout Browser - {self.current_viewport.title}")
self.update_viewport_display()
self.status_var.set("Welcome page loaded with viewport system")
def navigate_to_url(self, event=None):
"""Navigate to the URL in the address bar"""
url = self.url_var.get().strip()
if not url:
return
self.status_var.set(f"Loading {url}...")
self.root.update()
# Get current canvas size for viewport
self.root.update_idletasks()
canvas_width = max(800, self.canvas.winfo_width())
canvas_height = max(600, self.canvas.winfo_height())
try:
parser = HTMLViewportAdapter()
if url.startswith(('http://', 'https://')):
# Web URL
response = requests.get(url, timeout=10)
response.raise_for_status()
html_content = response.text
self.current_viewport = parser.parse_html_string(html_content, url, (canvas_width, canvas_height))
elif os.path.isfile(url):
# Local file
self.current_viewport = parser.parse_html_file(url, (canvas_width, canvas_height))
else:
# Try to treat as a local file path
if not url.startswith('file://'):
url = 'file://' + os.path.abspath(url)
file_path = url.replace('file://', '')
if os.path.isfile(file_path):
self.current_viewport = parser.parse_html_file(file_path, (canvas_width, canvas_height))
else:
raise FileNotFoundError(f"File not found: {file_path}")
# Update window title
if hasattr(self.current_viewport, 'title'):
self.root.title(f"pyWebLayout Browser - {self.current_viewport.title}")
# Add to history
self.add_to_history(url)
self.update_viewport_display()
self.status_var.set(f"Loaded {url}")
except Exception as e:
self.status_var.set(f"Error loading {url}: {str(e)}")
messagebox.showerror("Error", f"Failed to load {url}:\n{str(e)}")
def open_file(self):
"""Open a local HTML file"""
file_path = filedialog.askopenfilename(
title="Open HTML File",
filetypes=[("HTML files", "*.html *.htm"), ("All files", "*.*")]
)
if file_path:
self.url_var.set(file_path)
self.navigate_to_url()
def on_click(self, event):
"""Handle mouse clicks on the canvas"""
if not self.current_viewport:
return
# Convert canvas coordinates to viewport coordinates
canvas_x = self.canvas.canvasx(event.x)
canvas_y = self.canvas.canvasy(event.y)
# Use viewport hit testing
hit_element = self.current_viewport.hit_test((canvas_x, canvas_y))
if hit_element and hasattr(hit_element, '_callback'):
# Handle clickable elements
try:
result = hit_element._callback()
if result:
self.status_var.set(result)
# For external links, open in system browser
if hasattr(hit_element, '_link') and hit_element._link.link_type == LinkType.EXTERNAL:
webbrowser.open(hit_element._link.location)
except Exception as e:
self.status_var.set(f"Click error: {str(e)}")
def on_mouse_move(self, event):
"""Handle mouse movement for hover effects"""
if not self.current_viewport:
return
# Convert canvas coordinates to viewport coordinates
canvas_x = self.canvas.canvasx(event.x)
canvas_y = self.canvas.canvasy(event.y)
# Check if mouse is over any clickable element
hit_element = self.current_viewport.hit_test((canvas_x, canvas_y))
if hit_element and hasattr(hit_element, '_callback'):
self.canvas.configure(cursor="hand2")
else:
self.canvas.configure(cursor="arrow")
def add_to_history(self, url):
"""Add URL to navigation history"""
# Remove any forward history
self.history = self.history[:self.history_index + 1]
# Add new URL
self.history.append(url)
self.history_index = len(self.history) - 1
# Update navigation buttons
self.update_nav_buttons()
def update_nav_buttons(self):
"""Update the state of navigation buttons"""
self.back_btn.configure(state=tk.NORMAL if self.history_index > 0 else tk.DISABLED)
self.forward_btn.configure(state=tk.NORMAL if self.history_index < len(self.history) - 1 else tk.DISABLED)
def go_back(self):
"""Navigate back in history"""
if self.history_index > 0:
self.history_index -= 1
url = self.history[self.history_index]
self.url_var.set(url)
self.navigate_to_url()
def go_forward(self):
"""Navigate forward in history"""
if self.history_index < len(self.history) - 1:
self.history_index += 1
url = self.history[self.history_index]
self.url_var.set(url)
self.navigate_to_url()
def refresh(self):
"""Refresh the current page"""
current_url = self.url_var.get()
if current_url:
self.navigate_to_url()
else:
self.load_default_page()
def run(self):
"""Start the browser"""
self.root.mainloop()
def main():
"""Main function to run the enhanced browser"""
print("Starting pyWebLayout HTML Browser with Viewport System...")
try:
browser = ViewportBrowserWindow()
browser.run()
except Exception as e:
print(f"Error starting browser: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Page for pyWebLayout Browser</title>
</head>
<body>
<h1>pyWebLayout Browser Test Page</h1>
<h3>Images</h3>
<p>Here's a sample image:</p>
<img src="tests/data/sample_image.jpg" alt="Sample Image" width="200" height="150">
<h2>Text Formatting</h2>
<p>This is a paragraph with <b>bold text</b>, <i>italic text</i>, and <u>underlined text</u>.</p>
<h3>Links</h3>
<p>Here are some test links:</p>
<ul>
<li><a href="https://www.google.com" title="Google">External link to Google</a></li>
<li><a href="#section1" title="Section 1">Internal link to Section 1</a></li>
</ul>
<h3>Headers</h3>
<h1>H1 Header</h1>
<h2>H2 Header</h2>
<h3>H3 Header</h3>
<h4>H4 Header</h4>
<h5>H5 Header</h5>
<h6>H6 Header</h6>
<h3>Line Breaks and Paragraphs</h3>
<p>This is the first paragraph.</p>
<br>
<p>This is the second paragraph after a line break.</p>
<p>
It transpired after a confused five minutes that the man had heard Gatsbys name around his office in a connection which he either wouldnt reveal or didnt fully understand. This was his day off and with laudable initiative he had hurried out “to see.”
</p>
<p>
It was a random shot, and yet the reporters instinct was right. Gatsbys notoriety, spread about by the hundreds who had accepted his hospitality and so become authorities upon his past, had increased all summer until he fell just short of being news. Contemporary legends such as the “underground pipeline to Canada” attached themselves to him, and there was one persistent story that he didnt live in a house at all, but in a boat that looked like a house and was moved secretly up and down the Long Island shore. Just why these inventions were a source of satisfaction to James Gatz of North Dakota, isnt easy to say.
</p>
<p>
James Gatz—that was really, or at least legally, his name. He had changed it at the age of seventeen and at the specific moment that witnessed the beginning of his career—when he saw Dan Codys yacht drop anchor over the most insidious flat on Lake Superior. It was James Gatz who had been loafing along the beach that afternoon in a torn green jersey and a pair of canvas pants, but it was already Jay Gatsby who borrowed a rowboat, pulled out to the <i>Tuolomee</i>, and informed Cody that a wind might catch him and break him up in half an hour.
</p>
<p>
I suppose hed had the name ready for a long time, even then. His parents were shiftless and unsuccessful farm people—his imagination had never really accepted them as his parents at all. The truth was that Jay Gatsby of West Egg, Long Island, sprang from his Platonic conception of himself. He was a son of God—a phrase which, if it means anything, means just that—and he must be about His Fathers business, the service of a vast, vulgar, and meretricious beauty. So he invented just the sort of Jay Gatsby that a seventeen-year-old boy would be likely to invent, and to this conception he was faithful to the end.
</p>
<h3 id="section1">Section 1</h3>
<p>This is the content of section 1. You can link to this section using the internal link above.</p>
<h3>Images</h3>
<p>Here's a sample image:</p>
<img src="tests/data/sample_image.jpg" alt="Sample Image" width="200" height="150">
<h3>Mixed Content</h3>
<p>This paragraph contains <b>bold</b> and <i>italic</i> text, as well as an <a href="https://www.example.com">external link</a>.</p>
<p><strong>Strong text</strong> and <em>emphasized text</em> should also work.</p>
</body>
</html>