This commit is contained in:
@@ -6,6 +6,7 @@ import urllib.request
|
||||
import urllib.parse
|
||||
from PIL import Image as PILImage
|
||||
from .inline import Word, FormattedSpan
|
||||
from ..style import Font, FontWeight, FontStyle, TextDecoration
|
||||
|
||||
|
||||
class BlockType(Enum):
|
||||
@@ -72,6 +73,7 @@ class Paragraph(Block):
|
||||
self._words: List[Word] = []
|
||||
self._spans: List[FormattedSpan] = []
|
||||
self._style = style
|
||||
self._fonts: Dict[str, Font] = {} # Local font registry
|
||||
|
||||
@classmethod
|
||||
def create_and_add_to(cls, container, style=None) -> 'Paragraph':
|
||||
@@ -190,8 +192,88 @@ class Paragraph(Block):
|
||||
return len(self._words)
|
||||
|
||||
def __len__(self):
|
||||
|
||||
return self.word_count
|
||||
|
||||
def get_or_create_font(self,
|
||||
font_path: Optional[str] = None,
|
||||
font_size: int = 16,
|
||||
colour: Tuple[int, int, int] = (0, 0, 0),
|
||||
weight: FontWeight = FontWeight.NORMAL,
|
||||
style: FontStyle = FontStyle.NORMAL,
|
||||
decoration: TextDecoration = TextDecoration.NONE,
|
||||
background: Optional[Tuple[int, int, int, int]] = None,
|
||||
language: str = "en_EN",
|
||||
min_hyphenation_width: Optional[int] = None) -> Font:
|
||||
"""
|
||||
Get or create a font with the specified properties. Cascades to parent if available.
|
||||
|
||||
Args:
|
||||
font_path: Path to the font file (.ttf, .otf). If None, uses default font.
|
||||
font_size: Size of the font in points.
|
||||
colour: RGB color tuple for the text.
|
||||
weight: Font weight (normal or bold).
|
||||
style: Font style (normal or italic).
|
||||
decoration: Text decoration (none, underline, or strikethrough).
|
||||
background: RGBA background color for the text. If None, transparent background.
|
||||
language: Language code for hyphenation and text processing.
|
||||
min_hyphenation_width: Minimum width in pixels required for hyphenation.
|
||||
|
||||
Returns:
|
||||
Font object (either existing or newly created)
|
||||
"""
|
||||
# If we have a parent with font management, delegate to parent
|
||||
if self._parent and hasattr(self._parent, 'get_or_create_font'):
|
||||
return self._parent.get_or_create_font(
|
||||
font_path=font_path,
|
||||
font_size=font_size,
|
||||
colour=colour,
|
||||
weight=weight,
|
||||
style=style,
|
||||
decoration=decoration,
|
||||
background=background,
|
||||
language=language,
|
||||
min_hyphenation_width=min_hyphenation_width
|
||||
)
|
||||
|
||||
# Otherwise manage our own fonts
|
||||
# Create a unique key for this font configuration
|
||||
bg_tuple = background if background else (255, 255, 255, 0)
|
||||
min_hyph_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4
|
||||
|
||||
font_key = (
|
||||
font_path,
|
||||
font_size,
|
||||
colour,
|
||||
weight.value if isinstance(weight, FontWeight) else weight,
|
||||
style.value if isinstance(style, FontStyle) else style,
|
||||
decoration.value if isinstance(decoration, TextDecoration) else decoration,
|
||||
bg_tuple,
|
||||
language,
|
||||
min_hyph_width
|
||||
)
|
||||
|
||||
# Convert tuple to string for dictionary key
|
||||
key_str = str(font_key)
|
||||
|
||||
# Check if we already have this font
|
||||
if key_str in self._fonts:
|
||||
return self._fonts[key_str]
|
||||
|
||||
# Create new font and store it
|
||||
new_font = Font(
|
||||
font_path=font_path,
|
||||
font_size=font_size,
|
||||
colour=colour,
|
||||
weight=weight,
|
||||
style=style,
|
||||
decoration=decoration,
|
||||
background=background,
|
||||
language=language,
|
||||
min_hyphenation_width=min_hyphenation_width
|
||||
)
|
||||
|
||||
self._fonts[key_str] = new_font
|
||||
return new_font
|
||||
|
||||
|
||||
class HeadingLevel(Enum):
|
||||
|
||||
@@ -4,6 +4,7 @@ from enum import Enum
|
||||
from .block import Block, BlockType, Heading, HeadingLevel, Paragraph
|
||||
from .functional import Link, Button, Form
|
||||
from .inline import Word, FormattedSpan
|
||||
from ..style import Font, FontWeight, FontStyle, TextDecoration
|
||||
|
||||
|
||||
class MetadataType(Enum):
|
||||
@@ -43,7 +44,8 @@ class Document:
|
||||
self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets
|
||||
self._scripts: List[str] = [] # JavaScript code
|
||||
self._default_style = default_style
|
||||
|
||||
self._fonts: Dict[str, Font] = {} # Font registry for reusing font objects
|
||||
|
||||
# Set basic metadata
|
||||
if title:
|
||||
self.set_metadata(MetadataType.TITLE, title)
|
||||
@@ -302,6 +304,73 @@ class Document:
|
||||
toc.append((level, title, heading))
|
||||
|
||||
return toc
|
||||
|
||||
def get_or_create_font(self,
|
||||
font_path: Optional[str] = None,
|
||||
font_size: int = 16,
|
||||
colour: Tuple[int, int, int] = (0, 0, 0),
|
||||
weight: FontWeight = FontWeight.NORMAL,
|
||||
style: FontStyle = FontStyle.NORMAL,
|
||||
decoration: TextDecoration = TextDecoration.NONE,
|
||||
background: Optional[Tuple[int, int, int, int]] = None,
|
||||
language: str = "en_EN",
|
||||
min_hyphenation_width: Optional[int] = None) -> Font:
|
||||
"""
|
||||
Get or create a font with the specified properties. Reuses existing fonts
|
||||
when possible to avoid creating duplicate font objects.
|
||||
|
||||
Args:
|
||||
font_path: Path to the font file (.ttf, .otf). If None, uses default font.
|
||||
font_size: Size of the font in points.
|
||||
colour: RGB color tuple for the text.
|
||||
weight: Font weight (normal or bold).
|
||||
style: Font style (normal or italic).
|
||||
decoration: Text decoration (none, underline, or strikethrough).
|
||||
background: RGBA background color for the text. If None, transparent background.
|
||||
language: Language code for hyphenation and text processing.
|
||||
min_hyphenation_width: Minimum width in pixels required for hyphenation.
|
||||
|
||||
Returns:
|
||||
Font object (either existing or newly created)
|
||||
"""
|
||||
# Create a unique key for this font configuration
|
||||
bg_tuple = background if background else (255, 255, 255, 0)
|
||||
min_hyph_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4
|
||||
|
||||
font_key = (
|
||||
font_path,
|
||||
font_size,
|
||||
colour,
|
||||
weight.value if isinstance(weight, FontWeight) else weight,
|
||||
style.value if isinstance(style, FontStyle) else style,
|
||||
decoration.value if isinstance(decoration, TextDecoration) else decoration,
|
||||
bg_tuple,
|
||||
language,
|
||||
min_hyph_width
|
||||
)
|
||||
|
||||
# Convert tuple to string for dictionary key
|
||||
key_str = str(font_key)
|
||||
|
||||
# Check if we already have this font
|
||||
if key_str in self._fonts:
|
||||
return self._fonts[key_str]
|
||||
|
||||
# Create new font and store it
|
||||
new_font = Font(
|
||||
font_path=font_path,
|
||||
font_size=font_size,
|
||||
colour=colour,
|
||||
weight=weight,
|
||||
style=style,
|
||||
decoration=decoration,
|
||||
background=background,
|
||||
language=language,
|
||||
min_hyphenation_width=min_hyphenation_width
|
||||
)
|
||||
|
||||
self._fonts[key_str] = new_font
|
||||
return new_font
|
||||
|
||||
|
||||
class Chapter:
|
||||
@@ -310,7 +379,7 @@ class Chapter:
|
||||
A chapter contains a sequence of blocks and has metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, title: Optional[str] = None, level: int = 1, style=None):
|
||||
def __init__(self, title: Optional[str] = None, level: int = 1, style=None, parent=None):
|
||||
"""
|
||||
Initialize a new chapter.
|
||||
|
||||
@@ -318,12 +387,15 @@ class Chapter:
|
||||
title: The chapter title
|
||||
level: The chapter level (1 = top level, 2 = subsection, etc.)
|
||||
style: Optional default style for child blocks
|
||||
parent: Parent container (e.g., Document or Book)
|
||||
"""
|
||||
self._title = title
|
||||
self._level = level
|
||||
self._blocks: List[Block] = []
|
||||
self._metadata: Dict[str, Any] = {}
|
||||
self._style = style
|
||||
self._parent = parent
|
||||
self._fonts: Dict[str, Font] = {} # Local font registry
|
||||
|
||||
@property
|
||||
def title(self) -> Optional[str]:
|
||||
@@ -418,6 +490,87 @@ class Chapter:
|
||||
The metadata value, or None if not set
|
||||
"""
|
||||
return self._metadata.get(key)
|
||||
|
||||
def get_or_create_font(self,
|
||||
font_path: Optional[str] = None,
|
||||
font_size: int = 16,
|
||||
colour: Tuple[int, int, int] = (0, 0, 0),
|
||||
weight: FontWeight = FontWeight.NORMAL,
|
||||
style: FontStyle = FontStyle.NORMAL,
|
||||
decoration: TextDecoration = TextDecoration.NONE,
|
||||
background: Optional[Tuple[int, int, int, int]] = None,
|
||||
language: str = "en_EN",
|
||||
min_hyphenation_width: Optional[int] = None) -> Font:
|
||||
"""
|
||||
Get or create a font with the specified properties. Cascades to parent if available.
|
||||
|
||||
Args:
|
||||
font_path: Path to the font file (.ttf, .otf). If None, uses default font.
|
||||
font_size: Size of the font in points.
|
||||
colour: RGB color tuple for the text.
|
||||
weight: Font weight (normal or bold).
|
||||
style: Font style (normal or italic).
|
||||
decoration: Text decoration (none, underline, or strikethrough).
|
||||
background: RGBA background color for the text. If None, transparent background.
|
||||
language: Language code for hyphenation and text processing.
|
||||
min_hyphenation_width: Minimum width in pixels required for hyphenation.
|
||||
|
||||
Returns:
|
||||
Font object (either existing or newly created)
|
||||
"""
|
||||
# If we have a parent with font management, delegate to parent
|
||||
if self._parent and hasattr(self._parent, 'get_or_create_font'):
|
||||
return self._parent.get_or_create_font(
|
||||
font_path=font_path,
|
||||
font_size=font_size,
|
||||
colour=colour,
|
||||
weight=weight,
|
||||
style=style,
|
||||
decoration=decoration,
|
||||
background=background,
|
||||
language=language,
|
||||
min_hyphenation_width=min_hyphenation_width
|
||||
)
|
||||
|
||||
# Otherwise manage our own fonts
|
||||
# Create a unique key for this font configuration
|
||||
bg_tuple = background if background else (255, 255, 255, 0)
|
||||
min_hyph_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4
|
||||
|
||||
font_key = (
|
||||
font_path,
|
||||
font_size,
|
||||
colour,
|
||||
weight.value if isinstance(weight, FontWeight) else weight,
|
||||
style.value if isinstance(style, FontStyle) else style,
|
||||
decoration.value if isinstance(decoration, TextDecoration) else decoration,
|
||||
bg_tuple,
|
||||
language,
|
||||
min_hyph_width
|
||||
)
|
||||
|
||||
# Convert tuple to string for dictionary key
|
||||
key_str = str(font_key)
|
||||
|
||||
# Check if we already have this font
|
||||
if key_str in self._fonts:
|
||||
return self._fonts[key_str]
|
||||
|
||||
# Create new font and store it
|
||||
new_font = Font(
|
||||
font_path=font_path,
|
||||
font_size=font_size,
|
||||
colour=colour,
|
||||
weight=weight,
|
||||
style=style,
|
||||
decoration=decoration,
|
||||
background=background,
|
||||
language=language,
|
||||
min_hyphenation_width=min_hyphenation_width
|
||||
)
|
||||
|
||||
self._fonts[key_str] = new_font
|
||||
return new_font
|
||||
|
||||
|
||||
class Book(Document):
|
||||
|
||||
@@ -41,6 +41,7 @@ class StyleContext(NamedTuple):
|
||||
css_styles: Dict[str, str]
|
||||
element_attributes: Dict[str, Any]
|
||||
parent_elements: List[str] # Stack of parent element names
|
||||
document: Optional[Any] # Reference to document for font registry
|
||||
|
||||
def with_font(self, font: Font) -> "StyleContext":
|
||||
"""Create new context with modified font."""
|
||||
@@ -69,12 +70,13 @@ class StyleContext(NamedTuple):
|
||||
return self._replace(parent_elements=self.parent_elements + [element_name])
|
||||
|
||||
|
||||
def create_base_context(base_font: Optional[Font] = None) -> StyleContext:
|
||||
def create_base_context(base_font: Optional[Font] = None, document=None) -> StyleContext:
|
||||
"""
|
||||
Create a base style context with default values.
|
||||
|
||||
Args:
|
||||
base_font: Base font to use, defaults to system default
|
||||
document: Document instance for font registry
|
||||
|
||||
Returns:
|
||||
StyleContext with default values
|
||||
@@ -86,6 +88,7 @@ def create_base_context(base_font: Optional[Font] = None) -> StyleContext:
|
||||
css_styles={},
|
||||
element_attributes={},
|
||||
parent_elements=[],
|
||||
document=document,
|
||||
)
|
||||
|
||||
|
||||
@@ -125,7 +128,7 @@ def apply_element_styling(context: StyleContext, element: Tag) -> StyleContext:
|
||||
new_context = new_context.with_css_styles(css_styles)
|
||||
|
||||
# Apply element-specific default styles
|
||||
font = apply_element_font_styles(new_context.font, tag_name, css_styles)
|
||||
font = apply_element_font_styles(new_context.font, tag_name, css_styles, new_context)
|
||||
new_context = new_context.with_font(font)
|
||||
|
||||
# Apply background from styles
|
||||
@@ -154,18 +157,20 @@ def parse_inline_styles(style_text: str) -> Dict[str, str]:
|
||||
|
||||
|
||||
def apply_element_font_styles(
|
||||
font: Font, tag_name: str, css_styles: Dict[str, str]
|
||||
font: Font, tag_name: str, css_styles: Dict[str, str], context: Optional[StyleContext] = None
|
||||
) -> Font:
|
||||
"""
|
||||
Apply font styling based on HTML element and CSS styles.
|
||||
Uses document's font registry when available to avoid creating duplicate fonts.
|
||||
|
||||
Args:
|
||||
font: Current font
|
||||
tag_name: HTML tag name
|
||||
css_styles: CSS styles dictionary
|
||||
context: Style context with document reference for font registry
|
||||
|
||||
Returns:
|
||||
New Font object with applied styling
|
||||
Font object with applied styling (either existing or newly created)
|
||||
"""
|
||||
# Default element styles
|
||||
element_font_styles = {
|
||||
@@ -192,6 +197,7 @@ def apply_element_font_styles(
|
||||
decoration = font.decoration
|
||||
background = font.background
|
||||
language = font.language
|
||||
font_path = font._font_path
|
||||
|
||||
# Apply element default styles
|
||||
if tag_name in element_font_styles:
|
||||
@@ -264,16 +270,31 @@ def apply_element_font_styles(
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return Font(
|
||||
font_path=font._font_path,
|
||||
font_size=font_size,
|
||||
colour=colour,
|
||||
weight=weight,
|
||||
style=style,
|
||||
decoration=decoration,
|
||||
background=background,
|
||||
language=language,
|
||||
)
|
||||
# Use document's font registry if available to avoid creating duplicate fonts
|
||||
if context and context.document and hasattr(context.document, 'get_or_create_font'):
|
||||
return context.document.get_or_create_font(
|
||||
font_path=font_path,
|
||||
font_size=font_size,
|
||||
colour=colour,
|
||||
weight=weight,
|
||||
style=style,
|
||||
decoration=decoration,
|
||||
background=background,
|
||||
language=language,
|
||||
min_hyphenation_width=font.min_hyphenation_width
|
||||
)
|
||||
else:
|
||||
# Fallback to creating new font if no document context
|
||||
return Font(
|
||||
font_path=font_path,
|
||||
font_size=font_size,
|
||||
colour=colour,
|
||||
weight=weight,
|
||||
style=style,
|
||||
decoration=decoration,
|
||||
background=background,
|
||||
language=language,
|
||||
)
|
||||
|
||||
|
||||
def apply_background_styles(
|
||||
@@ -725,7 +746,7 @@ HANDLERS: Dict[str, Callable[[Tag, StyleContext], Union[Block, List[Block], None
|
||||
|
||||
|
||||
def parse_html_string(
|
||||
html_string: str, base_font: Optional[Font] = None
|
||||
html_string: str, base_font: Optional[Font] = None, document=None
|
||||
) -> List[Block]:
|
||||
"""
|
||||
Parse HTML string and return list of Block objects.
|
||||
@@ -733,12 +754,13 @@ def parse_html_string(
|
||||
Args:
|
||||
html_string: HTML content to parse
|
||||
base_font: Base font for styling, defaults to system default
|
||||
document: Document instance for font registry to avoid duplicate fonts
|
||||
|
||||
Returns:
|
||||
List of Block objects representing the document structure
|
||||
"""
|
||||
soup = BeautifulSoup(html_string, "html.parser")
|
||||
context = create_base_context(base_font)
|
||||
context = create_base_context(base_font, document)
|
||||
blocks = []
|
||||
|
||||
# Process the body if it exists, otherwise process all top-level elements
|
||||
|
||||
Reference in New Issue
Block a user