@@ -1,4 +1,4 @@
|
||||
from .block import Block, BlockType, Parapgraph, Heading, HeadingLevel, Quote, CodeBlock
|
||||
from .block import Block, BlockType, Paragraph, Heading, HeadingLevel, Quote, CodeBlock
|
||||
from .block import HList, ListItem, ListStyle, Table, TableRow, TableCell
|
||||
from .block import HorizontalRule, LineBreak, Image
|
||||
from .inline import Word, FormattedSpan
|
||||
|
||||
+540
-313
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import List, Dict, Optional, Tuple, Union, Any
|
||||
from enum import Enum
|
||||
from .block import Block, BlockType, Heading, HeadingLevel, Parapgraph
|
||||
from .block import Block, BlockType, Heading, HeadingLevel, Paragraph
|
||||
from .functional import Link, Button, Form
|
||||
from .inline import Word, FormattedSpan
|
||||
|
||||
@@ -27,13 +27,14 @@ class Document:
|
||||
This class manages the logical structure of the document without rendering concerns.
|
||||
"""
|
||||
|
||||
def __init__(self, title: Optional[str] = None, language: str = "en-US"):
|
||||
def __init__(self, title: Optional[str] = None, language: str = "en-US", default_style=None):
|
||||
"""
|
||||
Initialize a new document.
|
||||
|
||||
Args:
|
||||
title: The document title
|
||||
language: The document language code
|
||||
default_style: Optional default style for child blocks
|
||||
"""
|
||||
self._blocks: List[Block] = []
|
||||
self._metadata: Dict[MetadataType, Any] = {}
|
||||
@@ -41,6 +42,7 @@ class Document:
|
||||
self._resources: Dict[str, Any] = {} # External resources like images
|
||||
self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets
|
||||
self._scripts: List[str] = [] # JavaScript code
|
||||
self._default_style = default_style
|
||||
|
||||
# Set basic metadata
|
||||
if title:
|
||||
@@ -52,6 +54,16 @@ class Document:
|
||||
"""Get the top-level blocks in this document"""
|
||||
return self._blocks
|
||||
|
||||
@property
|
||||
def default_style(self):
|
||||
"""Get the default style for this document"""
|
||||
return self._default_style
|
||||
|
||||
@default_style.setter
|
||||
def default_style(self, style):
|
||||
"""Set the default style for this document"""
|
||||
self._default_style = style
|
||||
|
||||
def add_block(self, block: Block):
|
||||
"""
|
||||
Add a block to this document.
|
||||
@@ -61,6 +73,55 @@ class Document:
|
||||
"""
|
||||
self._blocks.append(block)
|
||||
|
||||
def create_paragraph(self, style=None) -> Paragraph:
|
||||
"""
|
||||
Create a new paragraph and add it to this document.
|
||||
|
||||
Args:
|
||||
style: Optional style override. If None, inherits from document
|
||||
|
||||
Returns:
|
||||
The newly created Paragraph object
|
||||
"""
|
||||
if style is None:
|
||||
style = self._default_style
|
||||
paragraph = Paragraph(style)
|
||||
self.add_block(paragraph)
|
||||
return paragraph
|
||||
|
||||
def create_heading(self, level: HeadingLevel = HeadingLevel.H1, style=None) -> Heading:
|
||||
"""
|
||||
Create a new heading and add it to this document.
|
||||
|
||||
Args:
|
||||
level: The heading level
|
||||
style: Optional style override. If None, inherits from document
|
||||
|
||||
Returns:
|
||||
The newly created Heading object
|
||||
"""
|
||||
if style is None:
|
||||
style = self._default_style
|
||||
heading = Heading(level, style)
|
||||
self.add_block(heading)
|
||||
return heading
|
||||
|
||||
def create_chapter(self, title: Optional[str] = None, level: int = 1, style=None) -> 'Chapter':
|
||||
"""
|
||||
Create a new chapter with inherited style.
|
||||
|
||||
Args:
|
||||
title: The chapter title
|
||||
level: The chapter level
|
||||
style: Optional style override. If None, inherits from document
|
||||
|
||||
Returns:
|
||||
The newly created Chapter object
|
||||
"""
|
||||
if style is None:
|
||||
style = self._default_style
|
||||
return Chapter(title, level, style)
|
||||
|
||||
def set_metadata(self, meta_type: MetadataType, value: Any):
|
||||
"""
|
||||
Set a metadata value.
|
||||
@@ -229,18 +290,20 @@ class Chapter:
|
||||
A chapter contains a sequence of blocks and has metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, title: Optional[str] = None, level: int = 1):
|
||||
def __init__(self, title: Optional[str] = None, level: int = 1, style=None):
|
||||
"""
|
||||
Initialize a new chapter.
|
||||
|
||||
Args:
|
||||
title: The chapter title
|
||||
level: The chapter level (1 = top level, 2 = subsection, etc.)
|
||||
style: Optional default style for child blocks
|
||||
"""
|
||||
self._title = title
|
||||
self._level = level
|
||||
self._blocks: List[Block] = []
|
||||
self._metadata: Dict[str, Any] = {}
|
||||
self._style = style
|
||||
|
||||
@property
|
||||
def title(self) -> Optional[str]:
|
||||
@@ -262,6 +325,16 @@ class Chapter:
|
||||
"""Get the blocks in this chapter"""
|
||||
return self._blocks
|
||||
|
||||
@property
|
||||
def style(self):
|
||||
"""Get the default style for this chapter"""
|
||||
return self._style
|
||||
|
||||
@style.setter
|
||||
def style(self, style):
|
||||
"""Set the default style for this chapter"""
|
||||
self._style = style
|
||||
|
||||
def add_block(self, block: Block):
|
||||
"""
|
||||
Add a block to this chapter.
|
||||
@@ -271,6 +344,39 @@ class Chapter:
|
||||
"""
|
||||
self._blocks.append(block)
|
||||
|
||||
def create_paragraph(self, style=None) -> Paragraph:
|
||||
"""
|
||||
Create a new paragraph and add it to this chapter.
|
||||
|
||||
Args:
|
||||
style: Optional style override. If None, inherits from chapter
|
||||
|
||||
Returns:
|
||||
The newly created Paragraph object
|
||||
"""
|
||||
if style is None:
|
||||
style = self._style
|
||||
paragraph = Paragraph(style)
|
||||
self.add_block(paragraph)
|
||||
return paragraph
|
||||
|
||||
def create_heading(self, level: HeadingLevel = HeadingLevel.H1, style=None) -> Heading:
|
||||
"""
|
||||
Create a new heading and add it to this chapter.
|
||||
|
||||
Args:
|
||||
level: The heading level
|
||||
style: Optional style override. If None, inherits from chapter
|
||||
|
||||
Returns:
|
||||
The newly created Heading object
|
||||
"""
|
||||
if style is None:
|
||||
style = self._style
|
||||
heading = Heading(level, style)
|
||||
self.add_block(heading)
|
||||
return heading
|
||||
|
||||
def set_metadata(self, key: str, value: Any):
|
||||
"""
|
||||
Set a metadata value.
|
||||
@@ -300,7 +406,8 @@ class Book(Document):
|
||||
A book is a document that contains chapters.
|
||||
"""
|
||||
|
||||
def __init__(self, title: Optional[str] = None, author: Optional[str] = None, language: str = "en-US"):
|
||||
def __init__(self, title: Optional[str] = None, author: Optional[str] = None,
|
||||
language: str = "en-US", default_style=None):
|
||||
"""
|
||||
Initialize a new book.
|
||||
|
||||
@@ -308,8 +415,9 @@ class Book(Document):
|
||||
title: The book title
|
||||
author: The book author
|
||||
language: The book language code
|
||||
default_style: Optional default style for child chapters and blocks
|
||||
"""
|
||||
super().__init__(title, language)
|
||||
super().__init__(title, language, default_style)
|
||||
self._chapters: List[Chapter] = []
|
||||
|
||||
if author:
|
||||
@@ -329,18 +437,21 @@ class Book(Document):
|
||||
"""
|
||||
self._chapters.append(chapter)
|
||||
|
||||
def create_chapter(self, title: Optional[str] = None, level: int = 1) -> Chapter:
|
||||
def create_chapter(self, title: Optional[str] = None, level: int = 1, style=None) -> Chapter:
|
||||
"""
|
||||
Create and add a new chapter.
|
||||
Create and add a new chapter with inherited style.
|
||||
|
||||
Args:
|
||||
title: The chapter title
|
||||
level: The chapter level
|
||||
style: Optional style override. If None, inherits from book
|
||||
|
||||
Returns:
|
||||
The new chapter
|
||||
"""
|
||||
chapter = Chapter(title, level)
|
||||
if style is None:
|
||||
style = self._default_style
|
||||
chapter = Chapter(title, level, style)
|
||||
self.add_chapter(chapter)
|
||||
return chapter
|
||||
|
||||
|
||||
@@ -27,6 +27,91 @@ class Word:
|
||||
self._previous = previous
|
||||
self._next = None
|
||||
self._hyphenated_parts = None # Will store hyphenated parts if word is hyphenated
|
||||
|
||||
@classmethod
|
||||
def create_and_add_to(cls, text: str, container, style: Optional[Font] = None,
|
||||
background=None) -> 'Word':
|
||||
"""
|
||||
Create a new Word and add it to a container, inheriting style and language
|
||||
from the container if not explicitly provided.
|
||||
|
||||
This method provides a convenient way to create words that automatically
|
||||
inherit styling from their container (Paragraph, FormattedSpan, etc.)
|
||||
without copying string values - using object references instead.
|
||||
|
||||
Args:
|
||||
text: The text content of the word
|
||||
container: The container to add the word to (must have add_word method and style property)
|
||||
style: Optional Font style override. If None, inherits from container
|
||||
background: Optional background color override. If None, inherits from container
|
||||
|
||||
Returns:
|
||||
The newly created Word object
|
||||
|
||||
Raises:
|
||||
AttributeError: If the container doesn't have the required add_word method or style property
|
||||
"""
|
||||
# Inherit style from container if not provided
|
||||
if style is None:
|
||||
if hasattr(container, 'style'):
|
||||
style = container.style
|
||||
else:
|
||||
raise AttributeError(f"Container {type(container).__name__} must have a 'style' property")
|
||||
|
||||
# Inherit background from container if not provided
|
||||
if background is None and hasattr(container, 'background'):
|
||||
background = container.background
|
||||
|
||||
# Determine the previous word for proper linking
|
||||
previous = None
|
||||
if hasattr(container, '_words') and container._words:
|
||||
# Container has a _words list (like FormattedSpan)
|
||||
previous = container._words[-1]
|
||||
elif hasattr(container, 'words'):
|
||||
# Container has a words() method (like Paragraph)
|
||||
try:
|
||||
# Get the last word from the iterator
|
||||
for _, word in container.words():
|
||||
previous = word
|
||||
except (StopIteration, TypeError):
|
||||
previous = None
|
||||
|
||||
# Create the new word
|
||||
word = cls(text, style, background, previous)
|
||||
|
||||
# Link the previous word to this new one
|
||||
if previous:
|
||||
previous.add_next(word)
|
||||
|
||||
# Add the word to the container
|
||||
if hasattr(container, 'add_word'):
|
||||
# Check if add_word expects a Word object or text string
|
||||
import inspect
|
||||
sig = inspect.signature(container.add_word)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
if len(params) > 0:
|
||||
# Peek at the parameter name to guess the expected type
|
||||
param_name = params[0]
|
||||
if param_name in ['word', 'word_obj', 'word_object']:
|
||||
# Expects a Word object
|
||||
container.add_word(word)
|
||||
else:
|
||||
# Might expect text string (like FormattedSpan.add_word)
|
||||
# In this case, we can't use the container's add_word as it would create
|
||||
# a duplicate Word. We need to add directly to the container's word list.
|
||||
if hasattr(container, '_words'):
|
||||
container._words.append(word)
|
||||
else:
|
||||
# Fallback: try calling with the Word object anyway
|
||||
container.add_word(word)
|
||||
else:
|
||||
# No parameters, shouldn't happen with add_word methods
|
||||
container.add_word(word)
|
||||
else:
|
||||
raise AttributeError(f"Container {type(container).__name__} must have an 'add_word' method")
|
||||
|
||||
return word
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
@@ -167,6 +252,45 @@ class FormattedSpan:
|
||||
self._background = background if background else style.background
|
||||
self._words: List[Word] = []
|
||||
|
||||
@classmethod
|
||||
def create_and_add_to(cls, container, style: Optional[Font] = None, background=None) -> 'FormattedSpan':
|
||||
"""
|
||||
Create a new FormattedSpan and add it to a container, inheriting style from
|
||||
the container if not explicitly provided.
|
||||
|
||||
Args:
|
||||
container: The container to add the span to (must have add_span method and style property)
|
||||
style: Optional Font style override. If None, inherits from container
|
||||
background: Optional background color override
|
||||
|
||||
Returns:
|
||||
The newly created FormattedSpan object
|
||||
|
||||
Raises:
|
||||
AttributeError: If the container doesn't have the required add_span method or style property
|
||||
"""
|
||||
# Inherit style from container if not provided
|
||||
if style is None:
|
||||
if hasattr(container, 'style'):
|
||||
style = container.style
|
||||
else:
|
||||
raise AttributeError(f"Container {type(container).__name__} must have a 'style' property")
|
||||
|
||||
# Inherit background from container if not provided
|
||||
if background is None and hasattr(container, 'background'):
|
||||
background = container.background
|
||||
|
||||
# Create the new span
|
||||
span = cls(style, background)
|
||||
|
||||
# Add the span to the container
|
||||
if hasattr(container, 'add_span'):
|
||||
container.add_span(span)
|
||||
else:
|
||||
raise AttributeError(f"Container {type(container).__name__} must have an 'add_span' method")
|
||||
|
||||
return span
|
||||
|
||||
@property
|
||||
def style(self) -> Font:
|
||||
"""Get the font style of this span"""
|
||||
|
||||
@@ -27,7 +27,7 @@ def main():
|
||||
parser.add_argument('epub_file', help='Path to EPUB file')
|
||||
parser.add_argument('--output-dir', '-o', default='output', help='Output directory for rendered pages')
|
||||
parser.add_argument('--width', '-w', type=int, default=800, help='Page width')
|
||||
parser.add_argument('--height', '-h', type=int, default=1000, help='Page height')
|
||||
parser.add_argument('--height', '-y', type=int, default=1000, help='Page height')
|
||||
parser.add_argument('--margin', '-m', type=int, default=50, help='Page margin')
|
||||
parser.add_argument('--max-pages', '-p', type=int, default=10, help='Maximum number of pages to render')
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple EPUB test script to isolate the issue.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the parent directory to the path to import pyWebLayout
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||
|
||||
def test_epub_basic():
|
||||
"""Test basic EPUB functionality without full HTML parsing."""
|
||||
print("Testing basic EPUB components...")
|
||||
|
||||
try:
|
||||
# Test basic document classes
|
||||
from pyWebLayout.abstract.document import Document, Book, Chapter, MetadataType
|
||||
print("✓ Document classes imported")
|
||||
|
||||
# Test creating a simple book
|
||||
book = Book("Test Book", "Test Author")
|
||||
chapter = book.create_chapter("Test Chapter")
|
||||
print("✓ Book and chapter created")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Basic test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_epub_file():
|
||||
"""Test opening the EPUB file without full parsing."""
|
||||
print("Testing EPUB file access...")
|
||||
|
||||
try:
|
||||
import zipfile
|
||||
import os
|
||||
|
||||
epub_path = "pg174-images-3.epub"
|
||||
if not os.path.exists(epub_path):
|
||||
print(f"✗ EPUB file not found: {epub_path}")
|
||||
return False
|
||||
|
||||
with zipfile.ZipFile(epub_path, 'r') as zip_ref:
|
||||
file_list = zip_ref.namelist()
|
||||
print(f"✓ EPUB file opened, contains {len(file_list)} files")
|
||||
|
||||
# Look for key files
|
||||
has_container = any('container.xml' in f for f in file_list)
|
||||
has_opf = any('.opf' in f for f in file_list)
|
||||
|
||||
print(f"✓ Container file: {'found' if has_container else 'not found'}")
|
||||
print(f"✓ Package file: {'found' if has_opf else 'not found'}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ EPUB file test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("Simple EPUB Test")
|
||||
print("=" * 50)
|
||||
|
||||
# Test basic functionality
|
||||
if not test_epub_basic():
|
||||
return False
|
||||
|
||||
print()
|
||||
|
||||
# Test EPUB file access
|
||||
if not test_epub_file():
|
||||
return False
|
||||
|
||||
print()
|
||||
print("All basic tests passed!")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -370,7 +370,7 @@ class EPUBReader:
|
||||
|
||||
# Parse HTML and add blocks to chapter
|
||||
base_url = os.path.dirname(path)
|
||||
document = parse_html(html, base_url)
|
||||
document = parse_html(html, base_url=base_url)
|
||||
|
||||
# Copy blocks to the chapter
|
||||
for block in document.blocks:
|
||||
@@ -381,8 +381,11 @@ class EPUBReader:
|
||||
# Add an error message block
|
||||
from pyWebLayout.abstract.block import Parapgraph
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font
|
||||
error_para = Parapgraph()
|
||||
error_para.add_word(Word(f"Error loading chapter: {str(e)}"))
|
||||
# Create a default font style for the error message
|
||||
default_font = Font()
|
||||
error_para.add_word(Word(f"Error loading chapter: {str(e)}", default_font))
|
||||
chapter.add_block(error_para)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user