This commit is contained in:
parent
49d4e551f8
commit
ea93681aaf
@ -7,6 +7,7 @@ import urllib.parse
|
|||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
from .inline import Word, FormattedSpan
|
from .inline import Word, FormattedSpan
|
||||||
from ..style import Font, FontWeight, FontStyle, TextDecoration
|
from ..style import Font, FontWeight, FontStyle, TextDecoration
|
||||||
|
from ..core import Hierarchical, Styleable, FontRegistry
|
||||||
|
|
||||||
|
|
||||||
class BlockType(Enum):
|
class BlockType(Enum):
|
||||||
@ -26,10 +27,12 @@ class BlockType(Enum):
|
|||||||
PAGE_BREAK = 13
|
PAGE_BREAK = 13
|
||||||
|
|
||||||
|
|
||||||
class Block:
|
class Block(Hierarchical):
|
||||||
"""
|
"""
|
||||||
Base class for all block-level elements.
|
Base class for all block-level elements.
|
||||||
Block elements typically represent visual blocks of content that stack vertically.
|
Block elements typically represent visual blocks of content that stack vertically.
|
||||||
|
|
||||||
|
Uses Hierarchical mixin for parent-child relationship management.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, block_type: BlockType):
|
def __init__(self, block_type: BlockType):
|
||||||
@ -39,28 +42,21 @@ class Block:
|
|||||||
Args:
|
Args:
|
||||||
block_type: The type of block this element represents
|
block_type: The type of block this element represents
|
||||||
"""
|
"""
|
||||||
|
super().__init__()
|
||||||
self._block_type = block_type
|
self._block_type = block_type
|
||||||
self._parent = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def block_type(self) -> BlockType:
|
def block_type(self) -> BlockType:
|
||||||
"""Get the type of this block element"""
|
"""Get the type of this block element"""
|
||||||
return self._block_type
|
return self._block_type
|
||||||
|
|
||||||
@property
|
|
||||||
def parent(self):
|
|
||||||
"""Get the parent block containing this block, if any"""
|
|
||||||
return self._parent
|
|
||||||
|
|
||||||
@parent.setter
|
class Paragraph(Styleable, FontRegistry, Block):
|
||||||
def parent(self, parent):
|
|
||||||
"""Set the parent block"""
|
|
||||||
self._parent = parent
|
|
||||||
|
|
||||||
|
|
||||||
class Paragraph(Block):
|
|
||||||
"""
|
"""
|
||||||
A paragraph is a block-level element that contains a sequence of words.
|
A paragraph is a block-level element that contains a sequence of words.
|
||||||
|
|
||||||
|
Uses Styleable mixin for style property management.
|
||||||
|
Uses FontRegistry mixin for font caching with parent delegation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, style=None):
|
def __init__(self, style=None):
|
||||||
@ -70,11 +66,9 @@ class Paragraph(Block):
|
|||||||
Args:
|
Args:
|
||||||
style: Optional default style for words in this paragraph
|
style: Optional default style for words in this paragraph
|
||||||
"""
|
"""
|
||||||
super().__init__(BlockType.PARAGRAPH)
|
super().__init__(style=style, block_type=BlockType.PARAGRAPH)
|
||||||
self._words: List[Word] = []
|
self._words: List[Word] = []
|
||||||
self._spans: List[FormattedSpan] = []
|
self._spans: List[FormattedSpan] = []
|
||||||
self._style : style = style
|
|
||||||
self._fonts: Dict[str, Font] = {} # Local font registry
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_and_add_to(cls, container, style=None) -> 'Paragraph':
|
def create_and_add_to(cls, container, style=None) -> 'Paragraph':
|
||||||
@ -109,16 +103,6 @@ class Paragraph(Block):
|
|||||||
|
|
||||||
return paragraph
|
return paragraph
|
||||||
|
|
||||||
@property
|
|
||||||
def style(self):
|
|
||||||
"""Get the default style for this paragraph"""
|
|
||||||
return self._style
|
|
||||||
|
|
||||||
@style.setter
|
|
||||||
def style(self, style):
|
|
||||||
"""Set the default style for this paragraph"""
|
|
||||||
self._style = style
|
|
||||||
|
|
||||||
def add_word(self, word: Word):
|
def add_word(self, word: Word):
|
||||||
"""
|
"""
|
||||||
Add a word to this paragraph.
|
Add a word to this paragraph.
|
||||||
@ -200,86 +184,7 @@ class Paragraph(Block):
|
|||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self.word_count
|
return self.word_count
|
||||||
|
|
||||||
def get_or_create_font(self,
|
# get_or_create_font() is provided by FontRegistry mixin
|
||||||
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):
|
class HeadingLevel(Enum):
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from .inline import Word, FormattedSpan
|
|||||||
from ..style import Font, FontWeight, FontStyle, TextDecoration
|
from ..style import Font, FontWeight, FontStyle, TextDecoration
|
||||||
from ..style.abstract_style import AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize
|
from ..style.abstract_style import AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize
|
||||||
from ..style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
|
from ..style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
|
||||||
|
from ..core import FontRegistry, MetadataContainer
|
||||||
|
|
||||||
|
|
||||||
class MetadataType(Enum):
|
class MetadataType(Enum):
|
||||||
@ -24,10 +25,13 @@ class MetadataType(Enum):
|
|||||||
CUSTOM = 100
|
CUSTOM = 100
|
||||||
|
|
||||||
|
|
||||||
class Document:
|
class Document(FontRegistry, MetadataContainer):
|
||||||
"""
|
"""
|
||||||
Abstract representation of a complete document like an HTML page or an ebook.
|
Abstract representation of a complete document like an HTML page or an ebook.
|
||||||
This class manages the logical structure of the document without rendering concerns.
|
This class manages the logical structure of the document without rendering concerns.
|
||||||
|
|
||||||
|
Uses FontRegistry mixin for font caching.
|
||||||
|
Uses MetadataContainer mixin for metadata management.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, title: Optional[str] = None, language: str = "en-US", default_style=None):
|
def __init__(self, title: Optional[str] = None, language: str = "en-US", default_style=None):
|
||||||
@ -39,13 +43,12 @@ class Document:
|
|||||||
language: The document language code
|
language: The document language code
|
||||||
default_style: Optional default style for child blocks
|
default_style: Optional default style for child blocks
|
||||||
"""
|
"""
|
||||||
|
super().__init__()
|
||||||
self._blocks: List[Block] = []
|
self._blocks: List[Block] = []
|
||||||
self._metadata: Dict[MetadataType, Any] = {}
|
|
||||||
self._anchors: Dict[str, Block] = {} # Named anchors for navigation
|
self._anchors: Dict[str, Block] = {} # Named anchors for navigation
|
||||||
self._resources: Dict[str, Any] = {} # External resources like images
|
self._resources: Dict[str, Any] = {} # External resources like images
|
||||||
self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets
|
self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets
|
||||||
self._scripts: List[str] = [] # JavaScript code
|
self._scripts: List[str] = [] # JavaScript code
|
||||||
self._fonts: Dict[str, Font] = {} # Font registry for backward compatibility
|
|
||||||
|
|
||||||
# Style management with new abstract/concrete system
|
# Style management with new abstract/concrete system
|
||||||
self._abstract_style_registry = AbstractStyleRegistry()
|
self._abstract_style_registry = AbstractStyleRegistry()
|
||||||
@ -146,27 +149,7 @@ class Document:
|
|||||||
style = self._default_style
|
style = self._default_style
|
||||||
return Chapter(title, level, style)
|
return Chapter(title, level, style)
|
||||||
|
|
||||||
def set_metadata(self, meta_type: MetadataType, value: Any):
|
# set_metadata() and get_metadata() are provided by MetadataContainer mixin
|
||||||
"""
|
|
||||||
Set a metadata value.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
meta_type: The type of metadata
|
|
||||||
value: The metadata value
|
|
||||||
"""
|
|
||||||
self._metadata[meta_type] = value
|
|
||||||
|
|
||||||
def get_metadata(self, meta_type: MetadataType) -> Optional[Any]:
|
|
||||||
"""
|
|
||||||
Get a metadata value.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
meta_type: The type of metadata
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The metadata value, or None if not set
|
|
||||||
"""
|
|
||||||
return self._metadata.get(meta_type)
|
|
||||||
|
|
||||||
def add_anchor(self, name: str, target: Block):
|
def add_anchor(self, name: str, target: Block):
|
||||||
"""
|
"""
|
||||||
@ -397,81 +380,16 @@ class Document:
|
|||||||
"""Get the concrete style registry for this document."""
|
"""Get the concrete style registry for this document."""
|
||||||
return self._concrete_style_registry
|
return self._concrete_style_registry
|
||||||
|
|
||||||
def get_or_create_font(self,
|
# get_or_create_font() is provided by FontRegistry mixin
|
||||||
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.
|
|
||||||
|
|
||||||
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)
|
|
||||||
"""
|
|
||||||
# Initialize font registry if it doesn't exist
|
|
||||||
if not hasattr(self, '_fonts'):
|
|
||||||
self._fonts: Dict[str, Font] = {}
|
|
||||||
|
|
||||||
# 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:
|
class Chapter(FontRegistry, MetadataContainer):
|
||||||
"""
|
"""
|
||||||
Represents a chapter or section in a document.
|
Represents a chapter or section in a document.
|
||||||
A chapter contains a sequence of blocks and has metadata.
|
A chapter contains a sequence of blocks and has metadata.
|
||||||
|
|
||||||
|
Uses FontRegistry mixin for font caching with parent delegation.
|
||||||
|
Uses MetadataContainer mixin for metadata management.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, title: Optional[str] = None, level: int = 1, style=None, parent=None):
|
def __init__(self, title: Optional[str] = None, level: int = 1, style=None, parent=None):
|
||||||
@ -484,13 +402,12 @@ class Chapter:
|
|||||||
style: Optional default style for child blocks
|
style: Optional default style for child blocks
|
||||||
parent: Parent container (e.g., Document or Book)
|
parent: Parent container (e.g., Document or Book)
|
||||||
"""
|
"""
|
||||||
|
super().__init__()
|
||||||
self._title = title
|
self._title = title
|
||||||
self._level = level
|
self._level = level
|
||||||
self._blocks: List[Block] = []
|
self._blocks: List[Block] = []
|
||||||
self._metadata: Dict[str, Any] = {}
|
|
||||||
self._style = style
|
self._style = style
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._fonts: Dict[str, Font] = {} # Local font registry
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self) -> Optional[str]:
|
def title(self) -> Optional[str]:
|
||||||
@ -564,108 +481,8 @@ class Chapter:
|
|||||||
self.add_block(heading)
|
self.add_block(heading)
|
||||||
return heading
|
return heading
|
||||||
|
|
||||||
def set_metadata(self, key: str, value: Any):
|
# set_metadata() and get_metadata() are provided by MetadataContainer mixin
|
||||||
"""
|
# get_or_create_font() is provided by FontRegistry mixin
|
||||||
Set a metadata value.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: The metadata key
|
|
||||||
value: The metadata value
|
|
||||||
"""
|
|
||||||
self._metadata[key] = value
|
|
||||||
|
|
||||||
def get_metadata(self, key: str) -> Optional[Any]:
|
|
||||||
"""
|
|
||||||
Get a metadata value.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: The metadata key
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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):
|
class Book(Document):
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pyWebLayout.core.base import Queriable
|
from pyWebLayout.core.base import Queriable
|
||||||
|
from pyWebLayout.core import Hierarchical
|
||||||
from pyWebLayout.style import Font
|
from pyWebLayout.style import Font
|
||||||
from pyWebLayout.style.abstract_style import AbstractStyle
|
from pyWebLayout.style.abstract_style import AbstractStyle
|
||||||
from typing import Tuple, Union, List, Optional, Dict, Any, Callable
|
from typing import Tuple, Union, List, Optional, Dict, Any, Callable
|
||||||
@ -358,35 +359,27 @@ class LinkedWord(Word):
|
|||||||
return self._location
|
return self._location
|
||||||
|
|
||||||
|
|
||||||
class LineBreak():
|
class LineBreak(Hierarchical):
|
||||||
"""
|
"""
|
||||||
A line break element that forces a new line within text content.
|
A line break element that forces a new line within text content.
|
||||||
While this is an inline element that can occur within paragraphs,
|
While this is an inline element that can occur within paragraphs,
|
||||||
it has block-like properties for consistency with the abstract model.
|
it has block-like properties for consistency with the abstract model.
|
||||||
|
|
||||||
|
Uses Hierarchical mixin for parent-child relationship management.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize a line break element."""
|
"""Initialize a line break element."""
|
||||||
|
super().__init__()
|
||||||
# Import here to avoid circular imports
|
# Import here to avoid circular imports
|
||||||
from .block import BlockType
|
from .block import BlockType
|
||||||
self._block_type = BlockType.LINE_BREAK
|
self._block_type = BlockType.LINE_BREAK
|
||||||
self._parent = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def block_type(self):
|
def block_type(self):
|
||||||
"""Get the block type for this line break"""
|
"""Get the block type for this line break"""
|
||||||
return self._block_type
|
return self._block_type
|
||||||
|
|
||||||
@property
|
|
||||||
def parent(self):
|
|
||||||
"""Get the parent element containing this line break, if any"""
|
|
||||||
return self._parent
|
|
||||||
|
|
||||||
@parent.setter
|
|
||||||
def parent(self, parent):
|
|
||||||
"""Set the parent element"""
|
|
||||||
self._parent = parent
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_and_add_to(cls, container) -> 'LineBreak':
|
def create_and_add_to(cls, container) -> 'LineBreak':
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -132,11 +132,15 @@ class InteractiveImage(Image, Interactable, Queriable):
|
|||||||
callback=callback
|
callback=callback
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to parent's children
|
# Add to parent using its add_block method
|
||||||
if hasattr(parent, 'add_child'):
|
if hasattr(parent, 'add_block'):
|
||||||
|
parent.add_block(img)
|
||||||
|
elif hasattr(parent, 'add_child'):
|
||||||
parent.add_child(img)
|
parent.add_child(img)
|
||||||
elif hasattr(parent, '_children'):
|
elif hasattr(parent, '_children'):
|
||||||
parent._children.append(img)
|
parent._children.append(img)
|
||||||
|
elif hasattr(parent, '_blocks'):
|
||||||
|
parent._blocks.append(img)
|
||||||
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,18 @@ from PIL import Image
|
|||||||
from typing import Tuple, Union, List, Optional, Dict
|
from typing import Tuple, Union, List, Optional, Dict
|
||||||
|
|
||||||
from pyWebLayout.core.base import Renderable, Queriable
|
from pyWebLayout.core.base import Renderable, Queriable
|
||||||
|
from pyWebLayout.core import Geometric
|
||||||
from pyWebLayout.style import Alignment
|
from pyWebLayout.style import Alignment
|
||||||
|
|
||||||
class Box(Renderable, Queriable):
|
class Box(Geometric, Renderable, Queriable):
|
||||||
|
"""
|
||||||
|
A box with geometric properties (origin and size).
|
||||||
|
|
||||||
|
Uses Geometric mixin for origin and size management.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self,origin, size, callback = None, sheet : Image = None, mode: bool = None, halign=Alignment.CENTER, valign = Alignment.CENTER):
|
def __init__(self,origin, size, callback = None, sheet : Image = None, mode: bool = None, halign=Alignment.CENTER, valign = Alignment.CENTER):
|
||||||
self._origin = np.array(origin)
|
super().__init__(origin=origin, size=size)
|
||||||
self._size = np.array(size)
|
|
||||||
self._end = self._origin + self._size
|
self._end = self._origin + self._size
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
self._sheet : Image = sheet
|
self._sheet : Image = sheet
|
||||||
@ -21,16 +26,7 @@ class Box(Renderable, Queriable):
|
|||||||
self._halign = halign
|
self._halign = halign
|
||||||
self._valign = valign
|
self._valign = valign
|
||||||
|
|
||||||
@property
|
# origin and size properties are provided by Geometric mixin
|
||||||
def origin(self) -> np.ndarray:
|
|
||||||
"""Get the origin (top-left corner) of the box"""
|
|
||||||
return self._origin
|
|
||||||
|
|
||||||
@property
|
|
||||||
def size(self) -> np.ndarray:
|
|
||||||
"""Get the size (width, height) of the box"""
|
|
||||||
return self._size
|
|
||||||
|
|
||||||
def in_shape(self, point):
|
def in_shape(self, point):
|
||||||
|
|
||||||
return np.all((point >= self._origin) & (point < self._end), axis=-1)
|
return np.all((point >= self._origin) & (point < self._end), axis=-1)
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from dataclasses import dataclass
|
|||||||
from pyWebLayout.core.base import Renderable, Queriable
|
from pyWebLayout.core.base import Renderable, Queriable
|
||||||
from pyWebLayout.concrete.box import Box
|
from pyWebLayout.concrete.box import Box
|
||||||
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph, Heading, Image as AbstractImage
|
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph, Heading, Image as AbstractImage
|
||||||
|
from pyWebLayout.abstract.interactive_image import InteractiveImage
|
||||||
from pyWebLayout.style import Font, Alignment
|
from pyWebLayout.style import Font, Alignment
|
||||||
|
|
||||||
|
|
||||||
@ -220,6 +221,13 @@ class TableCellRenderer(Box):
|
|||||||
text_y = y + (new_height - 12) // 2
|
text_y = y + (new_height - 12) // 2
|
||||||
self._draw.text((text_x, text_y), text, fill=(100, 100, 100), font=small_font)
|
self._draw.text((text_x, text_y), text, fill=(100, 100, 100), font=small_font)
|
||||||
|
|
||||||
|
# Set bounds on InteractiveImage objects for tap detection
|
||||||
|
if isinstance(image_block, InteractiveImage):
|
||||||
|
image_block.set_rendered_bounds(
|
||||||
|
origin=(img_x, y),
|
||||||
|
size=(new_width, new_height)
|
||||||
|
)
|
||||||
|
|
||||||
return y + new_height + 5 # Add some spacing after image
|
return y + new_height + 5 # Add some spacing after image
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -6,5 +6,7 @@ of the pyWebLayout rendering system.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pyWebLayout.core.base import (
|
from pyWebLayout.core.base import (
|
||||||
Renderable, Interactable, Layoutable, Queriable
|
Renderable, Interactable, Layoutable, Queriable,
|
||||||
|
Hierarchical, Geometric, Styleable, FontRegistry,
|
||||||
|
MetadataContainer, BlockContainer, ContainerAware
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Optional, Tuple, List, TYPE_CHECKING
|
from typing import Optional, Tuple, List, TYPE_CHECKING, Any, Dict
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from pyWebLayout.style.alignment import Alignment
|
from pyWebLayout.style.alignment import Alignment
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pyWebLayout.core.query import QueryResult
|
from pyWebLayout.core.query import QueryResult
|
||||||
|
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||||
|
|
||||||
|
|
||||||
class Renderable(ABC):
|
class Renderable(ABC):
|
||||||
@ -74,3 +75,361 @@ class Queriable(ABC):
|
|||||||
point_array = np.array(point)
|
point_array = np.array(point)
|
||||||
relative_point = point_array - self._origin
|
relative_point = point_array - self._origin
|
||||||
return np.all((0 <= relative_point) & (relative_point < self.size))
|
return np.all((0 <= relative_point) & (relative_point < self.size))
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# Mixins - Reusable components for common patterns
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class Hierarchical:
|
||||||
|
"""
|
||||||
|
Mixin providing parent-child relationship management.
|
||||||
|
|
||||||
|
Classes using this mixin can track their parent in a document hierarchy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._parent: Optional[Any] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self) -> Optional[Any]:
|
||||||
|
"""Get the parent object containing this object, if any"""
|
||||||
|
return self._parent
|
||||||
|
|
||||||
|
@parent.setter
|
||||||
|
def parent(self, parent: Any):
|
||||||
|
"""Set the parent object"""
|
||||||
|
self._parent = parent
|
||||||
|
|
||||||
|
|
||||||
|
class Geometric:
|
||||||
|
"""
|
||||||
|
Mixin providing origin and size properties for positioned elements.
|
||||||
|
|
||||||
|
Provides standard geometric properties for elements that have a position
|
||||||
|
and size in 2D space. Uses numpy arrays for efficient calculations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, origin=None, size=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._origin = np.array(origin) if origin is not None else np.array([0, 0])
|
||||||
|
self._size = np.array(size) if size is not None else np.array([0, 0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origin(self) -> np.ndarray:
|
||||||
|
"""Get the origin (top-left corner) of the element"""
|
||||||
|
return self._origin
|
||||||
|
|
||||||
|
@origin.setter
|
||||||
|
def origin(self, origin: np.ndarray):
|
||||||
|
"""Set the origin of the element"""
|
||||||
|
self._origin = np.array(origin)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self) -> np.ndarray:
|
||||||
|
"""Get the size (width, height) of the element"""
|
||||||
|
return self._size
|
||||||
|
|
||||||
|
@size.setter
|
||||||
|
def size(self, size: np.ndarray):
|
||||||
|
"""Set the size of the element"""
|
||||||
|
self._size = np.array(size)
|
||||||
|
|
||||||
|
def set_origin(self, origin: np.ndarray):
|
||||||
|
"""Set the origin of this element (alternative setter method)"""
|
||||||
|
self._origin = np.array(origin)
|
||||||
|
|
||||||
|
|
||||||
|
class Styleable:
|
||||||
|
"""
|
||||||
|
Mixin providing style property management.
|
||||||
|
|
||||||
|
Classes using this mixin can have a style property that can be
|
||||||
|
inherited from parents or set explicitly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, style=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._style = style
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style(self) -> Optional[Any]:
|
||||||
|
"""Get the style for this element"""
|
||||||
|
return self._style
|
||||||
|
|
||||||
|
@style.setter
|
||||||
|
def style(self, style: Any):
|
||||||
|
"""Set the style for this element"""
|
||||||
|
self._style = style
|
||||||
|
|
||||||
|
|
||||||
|
class FontRegistry:
|
||||||
|
"""
|
||||||
|
Mixin providing font caching and creation with parent delegation.
|
||||||
|
|
||||||
|
This mixin allows classes to maintain a local font registry and create/reuse
|
||||||
|
Font objects efficiently. It supports parent delegation, where font requests
|
||||||
|
can cascade up to a parent container if one exists.
|
||||||
|
|
||||||
|
Classes using this mixin should also use Hierarchical to support parent delegation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._fonts: Dict[str, 'Font'] = {}
|
||||||
|
|
||||||
|
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' = None,
|
||||||
|
style: 'FontStyle' = None,
|
||||||
|
decoration: '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.
|
||||||
|
|
||||||
|
This method will first check if a parent object has a get_or_create_font
|
||||||
|
method and delegate to it. Otherwise, it will manage fonts locally.
|
||||||
|
|
||||||
|
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)
|
||||||
|
"""
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
|
||||||
|
|
||||||
|
# Set defaults for enum types
|
||||||
|
if weight is None:
|
||||||
|
weight = FontWeight.NORMAL
|
||||||
|
if style is None:
|
||||||
|
style = FontStyle.NORMAL
|
||||||
|
if decoration is None:
|
||||||
|
decoration = TextDecoration.NONE
|
||||||
|
|
||||||
|
# If we have a parent with font management, delegate to parent
|
||||||
|
if hasattr(self, '_parent') and 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 hasattr(weight, 'value') else weight,
|
||||||
|
style.value if hasattr(style, 'value') else style,
|
||||||
|
decoration.value if hasattr(decoration, 'value') 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 MetadataContainer:
|
||||||
|
"""
|
||||||
|
Mixin providing metadata dictionary management.
|
||||||
|
|
||||||
|
Allows classes to store and retrieve arbitrary metadata as key-value pairs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._metadata: Dict[Any, Any] = {}
|
||||||
|
|
||||||
|
def set_metadata(self, key: Any, value: Any):
|
||||||
|
"""
|
||||||
|
Set a metadata value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The metadata key
|
||||||
|
value: The metadata value
|
||||||
|
"""
|
||||||
|
self._metadata[key] = value
|
||||||
|
|
||||||
|
def get_metadata(self, key: Any) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
Get a metadata value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The metadata key
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The metadata value, or None if not set
|
||||||
|
"""
|
||||||
|
return self._metadata.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
class BlockContainer:
|
||||||
|
"""
|
||||||
|
Mixin providing block management methods.
|
||||||
|
|
||||||
|
Provides standard methods for managing block-level children including
|
||||||
|
adding blocks and creating common block types.
|
||||||
|
|
||||||
|
Classes using this mixin should also use Styleable to support style inheritance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._blocks = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blocks(self):
|
||||||
|
"""Get the list of blocks in this container"""
|
||||||
|
return self._blocks
|
||||||
|
|
||||||
|
def add_block(self, block):
|
||||||
|
"""
|
||||||
|
Add a block to this container.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block: The block to add
|
||||||
|
"""
|
||||||
|
self._blocks.append(block)
|
||||||
|
if hasattr(block, 'parent'):
|
||||||
|
block.parent = self
|
||||||
|
|
||||||
|
def create_paragraph(self, style=None):
|
||||||
|
"""
|
||||||
|
Create a new paragraph and add it to this container.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
style: Optional style override. If None, inherits from container
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created Paragraph object
|
||||||
|
"""
|
||||||
|
from pyWebLayout.abstract.block import Paragraph
|
||||||
|
|
||||||
|
if style is None and hasattr(self, '_style'):
|
||||||
|
style = self._style
|
||||||
|
|
||||||
|
paragraph = Paragraph(style)
|
||||||
|
self.add_block(paragraph)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
def create_heading(self, level=None, style=None):
|
||||||
|
"""
|
||||||
|
Create a new heading and add it to this container.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: The heading level (h1-h6)
|
||||||
|
style: Optional style override. If None, inherits from container
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The newly created Heading object
|
||||||
|
"""
|
||||||
|
from pyWebLayout.abstract.block import Heading, HeadingLevel
|
||||||
|
|
||||||
|
if level is None:
|
||||||
|
level = HeadingLevel.H1
|
||||||
|
|
||||||
|
if style is None and hasattr(self, '_style'):
|
||||||
|
style = self._style
|
||||||
|
|
||||||
|
heading = Heading(level, style)
|
||||||
|
self.add_block(heading)
|
||||||
|
return heading
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerAware:
|
||||||
|
"""
|
||||||
|
Mixin providing support for the create_and_add_to factory pattern.
|
||||||
|
|
||||||
|
This is a base that can be extended to provide the create_and_add_to
|
||||||
|
class method pattern used throughout the abstract module.
|
||||||
|
|
||||||
|
Note: This is a framework for future refactoring. Currently, each class
|
||||||
|
has its own create_and_add_to implementation due to varying constructor
|
||||||
|
signatures. This mixin provides a foundation for standardizing that pattern.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _validate_container(cls, container, required_method='add_block'):
|
||||||
|
"""
|
||||||
|
Validate that a container has the required method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container: The container to validate
|
||||||
|
required_method: The method name to check for
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: If the container doesn't have the required method
|
||||||
|
"""
|
||||||
|
if not hasattr(container, required_method):
|
||||||
|
raise AttributeError(
|
||||||
|
f"Container {type(container).__name__} must have a '{required_method}' method"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _inherit_style(cls, container, style=None):
|
||||||
|
"""
|
||||||
|
Inherit style from container if not explicitly provided.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container: The container to inherit from
|
||||||
|
style: Optional explicit style
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The style to use (explicit or inherited)
|
||||||
|
"""
|
||||||
|
if style is not None:
|
||||||
|
return style
|
||||||
|
|
||||||
|
if hasattr(container, 'style'):
|
||||||
|
return container.style
|
||||||
|
elif hasattr(container, 'default_style'):
|
||||||
|
return container.default_style
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
58
tests/abstract/test_document_mixins.py
Normal file
58
tests/abstract/test_document_mixins.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""
|
||||||
|
Simplified unit tests for abstract document elements using test mixins.
|
||||||
|
|
||||||
|
This demonstrates how test mixins can eliminate duplication and simplify tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from pyWebLayout.abstract.document import Document, Chapter
|
||||||
|
from tests.mixins.font_registry_tests import FontRegistryTestMixin, FontRegistryParentDelegationTestMixin
|
||||||
|
from tests.mixins.metadata_tests import MetadataContainerTestMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestDocumentFontRegistry(FontRegistryTestMixin, unittest.TestCase):
|
||||||
|
"""Test FontRegistry behavior for Document - simplified with mixin."""
|
||||||
|
|
||||||
|
def create_test_object(self):
|
||||||
|
"""Create a Document instance for testing."""
|
||||||
|
return Document("Test Document", "en-US")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDocumentMetadata(MetadataContainerTestMixin, unittest.TestCase):
|
||||||
|
"""Test MetadataContainer behavior for Document - simplified with mixin."""
|
||||||
|
|
||||||
|
def create_test_object(self):
|
||||||
|
"""Create a Document instance for testing."""
|
||||||
|
return Document("Test Document", "en-US")
|
||||||
|
|
||||||
|
|
||||||
|
class TestChapterFontRegistry(FontRegistryTestMixin, unittest.TestCase):
|
||||||
|
"""Test FontRegistry behavior for Chapter - simplified with mixin."""
|
||||||
|
|
||||||
|
def create_test_object(self):
|
||||||
|
"""Create a Chapter instance for testing."""
|
||||||
|
return Chapter("Test Chapter", level=1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestChapterFontRegistryParentDelegation(FontRegistryParentDelegationTestMixin, unittest.TestCase):
|
||||||
|
"""Test FontRegistry parent delegation for Chapter - simplified with mixin."""
|
||||||
|
|
||||||
|
def create_parent(self):
|
||||||
|
"""Create a Document as parent."""
|
||||||
|
return Document("Parent Document", "en-US")
|
||||||
|
|
||||||
|
def create_child(self, parent):
|
||||||
|
"""Create a Chapter with parent reference."""
|
||||||
|
return Chapter("Child Chapter", level=1, parent=parent)
|
||||||
|
|
||||||
|
|
||||||
|
class TestChapterMetadata(MetadataContainerTestMixin, unittest.TestCase):
|
||||||
|
"""Test MetadataContainer behavior for Chapter - simplified with mixin."""
|
||||||
|
|
||||||
|
def create_test_object(self):
|
||||||
|
"""Create a Chapter instance for testing."""
|
||||||
|
return Chapter("Test Chapter", level=1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
0
tests/mixins/__init__.py
Normal file
0
tests/mixins/__init__.py
Normal file
116
tests/mixins/font_registry_tests.py
Normal file
116
tests/mixins/font_registry_tests.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Test mixins for FontRegistry functionality.
|
||||||
|
|
||||||
|
Provides reusable test cases for any class that uses the FontRegistry mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyWebLayout.style import FontWeight, FontStyle, TextDecoration
|
||||||
|
|
||||||
|
|
||||||
|
class FontRegistryTestMixin:
|
||||||
|
"""
|
||||||
|
Mixin providing standard tests for FontRegistry behavior.
|
||||||
|
|
||||||
|
Classes using this mixin must implement:
|
||||||
|
- create_test_object() -> object with FontRegistry mixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_test_object(self):
|
||||||
|
"""
|
||||||
|
Create an instance of the object to test.
|
||||||
|
Must be implemented by test class.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Test class must implement create_test_object()")
|
||||||
|
|
||||||
|
def test_font_caching(self):
|
||||||
|
"""Test that fonts with identical properties are cached and reused."""
|
||||||
|
obj = self.create_test_object()
|
||||||
|
|
||||||
|
# Create font twice with same properties
|
||||||
|
font1 = obj.get_or_create_font(font_size=14, colour=(255, 0, 0), weight=FontWeight.BOLD)
|
||||||
|
font2 = obj.get_or_create_font(font_size=14, colour=(255, 0, 0), weight=FontWeight.BOLD)
|
||||||
|
|
||||||
|
# Should return the same font object (cached)
|
||||||
|
self.assertIs(font1, font2, "Fonts with identical properties should be cached")
|
||||||
|
|
||||||
|
# Should only have one font in registry
|
||||||
|
self.assertEqual(len(obj._fonts), 1, "Registry should contain exactly one font")
|
||||||
|
|
||||||
|
def test_font_differentiation(self):
|
||||||
|
"""Test that fonts with different properties create separate instances."""
|
||||||
|
obj = self.create_test_object()
|
||||||
|
|
||||||
|
# Create fonts that differ in exactly one property each
|
||||||
|
base_params = {'colour': (0, 0, 0), 'weight': FontWeight.NORMAL}
|
||||||
|
|
||||||
|
font1 = obj.get_or_create_font(font_size=14, **base_params)
|
||||||
|
font2 = obj.get_or_create_font(font_size=16, **base_params) # Different size
|
||||||
|
|
||||||
|
base_params2 = {'font_size': 18, 'weight': FontWeight.NORMAL}
|
||||||
|
font3 = obj.get_or_create_font(colour=(255, 0, 0), **base_params2) # Different color
|
||||||
|
|
||||||
|
base_params3 = {'font_size': 20, 'colour': (100, 100, 100)}
|
||||||
|
font4 = obj.get_or_create_font(weight=FontWeight.BOLD, **base_params3) # Different weight
|
||||||
|
|
||||||
|
# All should be different objects
|
||||||
|
self.assertIsNot(font1, font2, "Fonts with different sizes should be distinct")
|
||||||
|
self.assertIsNot(font1, font3, "Fonts with different colors should be distinct")
|
||||||
|
self.assertIsNot(font1, font4, "Fonts with different weights should be distinct")
|
||||||
|
self.assertIsNot(font2, font3, "Fonts should be distinct")
|
||||||
|
self.assertIsNot(font2, font4, "Fonts should be distinct")
|
||||||
|
self.assertIsNot(font3, font4, "Fonts should be distinct")
|
||||||
|
|
||||||
|
# Should have 4 fonts in registry
|
||||||
|
self.assertEqual(len(obj._fonts), 4, "Should have 4 distinct fonts")
|
||||||
|
|
||||||
|
def test_font_properties(self):
|
||||||
|
"""Test that created fonts have the correct properties."""
|
||||||
|
obj = self.create_test_object()
|
||||||
|
|
||||||
|
font = obj.get_or_create_font(
|
||||||
|
font_size=18,
|
||||||
|
colour=(128, 64, 32),
|
||||||
|
weight=FontWeight.BOLD,
|
||||||
|
style=FontStyle.ITALIC,
|
||||||
|
decoration=TextDecoration.UNDERLINE
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(font.font_size, 18)
|
||||||
|
self.assertEqual(font.colour, (128, 64, 32))
|
||||||
|
self.assertEqual(font.weight, FontWeight.BOLD)
|
||||||
|
self.assertEqual(font.style, FontStyle.ITALIC)
|
||||||
|
self.assertEqual(font.decoration, TextDecoration.UNDERLINE)
|
||||||
|
|
||||||
|
|
||||||
|
class FontRegistryParentDelegationTestMixin:
|
||||||
|
"""
|
||||||
|
Test mixin for FontRegistry parent delegation functionality.
|
||||||
|
|
||||||
|
Classes using this mixin must implement:
|
||||||
|
- create_parent() -> parent object with FontRegistry
|
||||||
|
- create_child(parent) -> child object with FontRegistry and parent link
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_parent(self):
|
||||||
|
"""Create a parent object with FontRegistry. Must be implemented."""
|
||||||
|
raise NotImplementedError("Test class must implement create_parent()")
|
||||||
|
|
||||||
|
def create_child(self, parent):
|
||||||
|
"""Create a child object with parent reference. Must be implemented."""
|
||||||
|
raise NotImplementedError("Test class must implement create_child(parent)")
|
||||||
|
|
||||||
|
def test_parent_delegation(self):
|
||||||
|
"""Test that font creation delegates to parent when available."""
|
||||||
|
parent = self.create_parent()
|
||||||
|
child = self.create_child(parent)
|
||||||
|
|
||||||
|
# Create font through child
|
||||||
|
font = child.get_or_create_font(font_size=16, colour=(200, 100, 50))
|
||||||
|
|
||||||
|
# Font should be in parent's registry, not child's
|
||||||
|
self.assertEqual(len(parent._fonts), 1, "Font should be in parent registry")
|
||||||
|
self.assertEqual(len(child._fonts), 0, "Font should NOT be in child registry")
|
||||||
|
|
||||||
|
# Getting same font through child should return parent's font
|
||||||
|
font2 = child.get_or_create_font(font_size=16, colour=(200, 100, 50))
|
||||||
|
self.assertIs(font, font2, "Child should reuse parent's font")
|
||||||
68
tests/mixins/metadata_tests.py
Normal file
68
tests/mixins/metadata_tests.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""
|
||||||
|
Test mixins for MetadataContainer functionality.
|
||||||
|
|
||||||
|
Provides reusable test cases for any class that uses the MetadataContainer mixin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataContainerTestMixin:
|
||||||
|
"""
|
||||||
|
Mixin providing standard tests for MetadataContainer behavior.
|
||||||
|
|
||||||
|
Classes using this mixin must implement:
|
||||||
|
- create_test_object() -> object with MetadataContainer mixin
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_test_object(self):
|
||||||
|
"""
|
||||||
|
Create an instance of the object to test.
|
||||||
|
Must be implemented by test class.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Test class must implement create_test_object()")
|
||||||
|
|
||||||
|
def test_set_and_get_metadata(self):
|
||||||
|
"""Test basic metadata storage and retrieval."""
|
||||||
|
obj = self.create_test_object()
|
||||||
|
|
||||||
|
# Set various types of metadata
|
||||||
|
obj.set_metadata("string_key", "string_value")
|
||||||
|
obj.set_metadata("int_key", 42)
|
||||||
|
obj.set_metadata("list_key", [1, 2, 3])
|
||||||
|
obj.set_metadata("dict_key", {"nested": "value"})
|
||||||
|
|
||||||
|
# Verify retrieval
|
||||||
|
self.assertEqual(obj.get_metadata("string_key"), "string_value")
|
||||||
|
self.assertEqual(obj.get_metadata("int_key"), 42)
|
||||||
|
self.assertEqual(obj.get_metadata("list_key"), [1, 2, 3])
|
||||||
|
self.assertEqual(obj.get_metadata("dict_key"), {"nested": "value"})
|
||||||
|
|
||||||
|
def test_get_nonexistent_metadata(self):
|
||||||
|
"""Test that getting nonexistent metadata returns None."""
|
||||||
|
obj = self.create_test_object()
|
||||||
|
|
||||||
|
result = obj.get_metadata("nonexistent_key")
|
||||||
|
self.assertIsNone(result, "Nonexistent metadata should return None")
|
||||||
|
|
||||||
|
def test_update_metadata(self):
|
||||||
|
"""Test that metadata values can be updated."""
|
||||||
|
obj = self.create_test_object()
|
||||||
|
|
||||||
|
# Set initial value
|
||||||
|
obj.set_metadata("key", "initial")
|
||||||
|
self.assertEqual(obj.get_metadata("key"), "initial")
|
||||||
|
|
||||||
|
# Update value
|
||||||
|
obj.set_metadata("key", "updated")
|
||||||
|
self.assertEqual(obj.get_metadata("key"), "updated", "Metadata should be updateable")
|
||||||
|
|
||||||
|
def test_metadata_isolation(self):
|
||||||
|
"""Test that metadata is isolated between instances."""
|
||||||
|
obj1 = self.create_test_object()
|
||||||
|
obj2 = self.create_test_object()
|
||||||
|
|
||||||
|
obj1.set_metadata("key", "value1")
|
||||||
|
obj2.set_metadata("key", "value2")
|
||||||
|
|
||||||
|
self.assertEqual(obj1.get_metadata("key"), "value1")
|
||||||
|
self.assertEqual(obj2.get_metadata("key"), "value2")
|
||||||
|
self.assertNotEqual(obj1.get_metadata("key"), obj2.get_metadata("key"))
|
||||||
0
tests/style/__init__.py
Normal file
0
tests/style/__init__.py
Normal file
Loading…
x
Reference in New Issue
Block a user