diff --git a/pyWebLayout/abstract/block.py b/pyWebLayout/abstract/block.py index 4f3bf87..51f9684 100644 --- a/pyWebLayout/abstract/block.py +++ b/pyWebLayout/abstract/block.py @@ -6,7 +6,7 @@ import urllib.request import urllib.parse from PIL import Image as PILImage from .inline import Word, FormattedSpan -from ..core import Hierarchical, Styleable, FontRegistry +from ..core import Hierarchical, Styleable, FontRegistry, ContainerAware, BlockContainer class BlockType(Enum): @@ -50,7 +50,7 @@ class Block(Hierarchical): return self._block_type -class Paragraph(Styleable, FontRegistry, Block): +class Paragraph(Styleable, FontRegistry, ContainerAware, Block): """ A paragraph is a block-level element that contains a sequence of words. @@ -85,22 +85,15 @@ class Paragraph(Styleable, FontRegistry, Block): Raises: AttributeError: If the container doesn't have the required add_block method """ - # Inherit style from container if not provided - if style is None and hasattr(container, 'style'): - style = container.style - elif style is None and hasattr(container, 'default_style'): - style = container.default_style + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container) + style = cls._inherit_style(container, style) # Create the new paragraph paragraph = cls(style) # Add the paragraph to the container - if hasattr(container, 'add_block'): - container.add_block(paragraph) - else: - raise AttributeError( - f"Container {type(container).__name__} must have an 'add_block' method" - ) + container.add_block(paragraph) return paragraph @@ -237,22 +230,15 @@ class Heading(Paragraph): Raises: AttributeError: If the container doesn't have the required add_block method """ - # Inherit style from container if not provided - if style is None and hasattr(container, 'style'): - style = container.style - elif style is None and hasattr(container, 'default_style'): - style = container.default_style + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container) + style = cls._inherit_style(container, style) # Create the new heading heading = cls(level, style) # Add the heading to the container - if hasattr(container, 'add_block'): - container.add_block(heading) - else: - raise AttributeError( - f"Container {type(container).__name__} must have an 'add_block' method" - ) + container.add_block(heading) return heading @@ -267,7 +253,7 @@ class Heading(Paragraph): self._level = level -class Quote(Block): +class Quote(BlockContainer, ContainerAware, Block): """ A blockquote element that can contain other block elements. """ @@ -280,7 +266,6 @@ class Quote(Block): style: Optional default style for child blocks """ super().__init__(BlockType.QUOTE) - self._blocks: List[Block] = [] self._style = style @classmethod @@ -299,22 +284,15 @@ class Quote(Block): Raises: AttributeError: If the container doesn't have the required add_block method """ - # Inherit style from container if not provided - if style is None and hasattr(container, 'style'): - style = container.style - elif style is None and hasattr(container, 'default_style'): - style = container.default_style + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container) + style = cls._inherit_style(container, style) # Create the new quote quote = cls(style) # Add the quote to the container - if hasattr(container, 'add_block'): - container.add_block(quote) - else: - raise AttributeError( - f"Container {type(container).__name__} must have an 'add_block' method" - ) + container.add_block(quote) return quote @@ -328,54 +306,6 @@ class Quote(Block): """Set the default style for this quote""" self._style = style - def add_block(self, block: Block): - """ - Add a block element to this quote. - - Args: - block: The Block object to add - """ - self._blocks.append(block) - block.parent = self - - def create_paragraph(self, style=None) -> Paragraph: - """ - Create a new paragraph and add it to this quote. - - Args: - style: Optional style override. If None, inherits from quote - - Returns: - The newly created Paragraph object - """ - return Paragraph.create_and_add_to(self, style) - - def create_heading( - self, - level: HeadingLevel = HeadingLevel.H1, - style=None) -> Heading: - """ - Create a new heading and add it to this quote. - - Args: - level: The heading level - style: Optional style override. If None, inherits from quote - - Returns: - The newly created Heading object - """ - return Heading.create_and_add_to(self, level, style) - - def blocks(self) -> Iterator[Block]: - """ - Iterate over the blocks in this quote. - - Yields: - Each Block in the quote - """ - for block in self._blocks: - yield block - class CodeBlock(Block): """ @@ -463,7 +393,7 @@ class ListStyle(Enum): DEFINITION = 3 #
-class HList(Block): +class HList(ContainerAware, Block): """ An HTML list element (ul, ol, dl). """ @@ -502,22 +432,15 @@ class HList(Block): Raises: AttributeError: If the container doesn't have the required add_block method """ - # Inherit style from container if not provided - if default_style is None and hasattr(container, 'style'): - default_style = container.style - elif default_style is None and hasattr(container, 'default_style'): - default_style = container.default_style + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container) + default_style = cls._inherit_style(container, default_style) # Create the new list hlist = cls(style, default_style) # Add the list to the container - if hasattr(container, 'add_block'): - container.add_block(hlist) - else: - raise AttributeError( - f"Container {type(container).__name__} must have an 'add_block' method" - ) + container.add_block(hlist) return hlist @@ -580,7 +503,7 @@ class HList(Block): return len(self._items) -class ListItem(Block): +class ListItem(BlockContainer, ContainerAware, Block): """ A list item element that can contain other block elements. """ @@ -594,7 +517,6 @@ class ListItem(Block): style: Optional default style for child blocks """ super().__init__(BlockType.LIST_ITEM) - self._blocks: List[Block] = [] self._term = term self._style = style @@ -619,22 +541,15 @@ class ListItem(Block): Raises: AttributeError: If the container doesn't have the required add_item method """ - # Inherit style from container if not provided - if style is None and hasattr(container, 'default_style'): - style = container.default_style - elif style is None and hasattr(container, 'style'): - style = container.style + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container, required_method='add_item') + style = cls._inherit_style(container, style) # Create the new list item item = cls(term, style) # Add the list item to the container - if hasattr(container, 'add_item'): - container.add_item(item) - else: - raise AttributeError( - f"Container {type(container).__name__} must have an 'add_item' method" - ) + container.add_item(item) return item @@ -658,56 +573,8 @@ class ListItem(Block): """Set the default style for this list item""" self._style = style - def add_block(self, block: Block): - """ - Add a block element to this list item. - Args: - block: The Block object to add - """ - self._blocks.append(block) - block.parent = self - - def create_paragraph(self, style=None) -> Paragraph: - """ - Create a new paragraph and add it to this list item. - - Args: - style: Optional style override. If None, inherits from list item - - Returns: - The newly created Paragraph object - """ - return Paragraph.create_and_add_to(self, style) - - def create_heading( - self, - level: HeadingLevel = HeadingLevel.H1, - style=None) -> Heading: - """ - Create a new heading and add it to this list item. - - Args: - level: The heading level - style: Optional style override. If None, inherits from list item - - Returns: - The newly created Heading object - """ - return Heading.create_and_add_to(self, level, style) - - def blocks(self) -> Iterator[Block]: - """ - Iterate over the blocks in this list item. - - Yields: - Each Block in the list item - """ - for block in self._blocks: - yield block - - -class TableCell(Block): +class TableCell(BlockContainer, ContainerAware, Block): """ A table cell element that can contain other block elements. """ @@ -731,7 +598,6 @@ class TableCell(Block): self._is_header = is_header self._colspan = colspan self._rowspan = rowspan - self._blocks: List[Block] = [] self._style = style @classmethod @@ -754,20 +620,15 @@ class TableCell(Block): Raises: AttributeError: If the container doesn't have the required add_cell method """ - # Inherit style from container if not provided - if style is None and hasattr(container, 'style'): - style = container.style + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container, required_method='add_cell') + style = cls._inherit_style(container, style) # Create the new table cell cell = cls(is_header, colspan, rowspan, style) # Add the cell to the container - if hasattr(container, 'add_cell'): - container.add_cell(cell) - else: - raise AttributeError( - f"Container {type(container).__name__} must have an 'add_cell' method" - ) + container.add_cell(cell) return cell @@ -811,56 +672,8 @@ class TableCell(Block): """Set the default style for this table cell""" self._style = style - def add_block(self, block: Block): - """ - Add a block element to this cell. - Args: - block: The Block object to add - """ - self._blocks.append(block) - block.parent = self - - def create_paragraph(self, style=None) -> Paragraph: - """ - Create a new paragraph and add it to this table cell. - - Args: - style: Optional style override. If None, inherits from cell - - Returns: - The newly created Paragraph object - """ - return Paragraph.create_and_add_to(self, style) - - def create_heading( - self, - level: HeadingLevel = HeadingLevel.H1, - style=None) -> Heading: - """ - Create a new heading and add it to this table cell. - - Args: - level: The heading level - style: Optional style override. If None, inherits from cell - - Returns: - The newly created Heading object - """ - return Heading.create_and_add_to(self, level, style) - - def blocks(self) -> Iterator[Block]: - """ - Iterate over the blocks in this cell. - - Yields: - Each Block in the cell - """ - for block in self._blocks: - yield block - - -class TableRow(Block): +class TableRow(ContainerAware, Block): """ A table row element containing table cells. """ @@ -897,20 +710,15 @@ class TableRow(Block): Raises: AttributeError: If the container doesn't have the required add_row method """ - # Inherit style from container if not provided - if style is None and hasattr(container, 'style'): - style = container.style + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container, required_method='add_row') + style = cls._inherit_style(container, style) # Create the new table row row = cls(style) # Add the row to the container - if hasattr(container, 'add_row'): - container.add_row(row, section) - else: - raise AttributeError( - f"Container {type(container).__name__} must have an 'add_row' method" - ) + container.add_row(row, section) return row @@ -970,7 +778,7 @@ class TableRow(Block): return len(self._cells) -class Table(Block): +class Table(ContainerAware, Block): """ A table element containing rows and cells. """ @@ -1011,22 +819,15 @@ class Table(Block): Raises: AttributeError: If the container doesn't have the required add_block method """ - # Inherit style from container if not provided - if style is None and hasattr(container, 'style'): - style = container.style - elif style is None and hasattr(container, 'default_style'): - style = container.default_style + # Validate container and inherit style using ContainerAware utilities + cls._validate_container(container) + style = cls._inherit_style(container, style) # Create the new table table = cls(caption, style) # Add the table to the container - if hasattr(container, 'add_block'): - container.add_block(table) - else: - raise AttributeError( - f"Container {type(container).__name__} must have an 'add_block' method" - ) + container.add_block(table) return table diff --git a/pyWebLayout/concrete/page.py b/pyWebLayout/concrete/page.py index 2dd6d12..c0b7973 100644 --- a/pyWebLayout/concrete/page.py +++ b/pyWebLayout/concrete/page.py @@ -190,6 +190,46 @@ class Page(Renderable, Queriable): """Get a copy of the children list""" return self._children.copy() + def _get_child_property(self, child: Renderable, private_attr: str, + public_attr: str, index: Optional[int] = None, + default: Optional[int] = None) -> Optional[int]: + """ + Generic helper to extract properties from child objects with multiple fallback strategies. + + Args: + child: The child object + private_attr: Name of the private attribute (e.g., '_size') + public_attr: Name of the public property (e.g., 'size') + index: Optional index for array-like properties (0 for width, 1 for height) + default: Default value if property cannot be determined + + Returns: + Property value or default + """ + # Try private attribute first + if hasattr(child, private_attr): + value = getattr(child, private_attr) + if value is not None: + if isinstance(value, (list, tuple, np.ndarray)): + if index is not None and len(value) > index: + return int(value[index]) + elif index is None: + return value + + # Try public property + if hasattr(child, public_attr): + value = getattr(child, public_attr) + if value is not None: + if isinstance(value, (list, tuple, np.ndarray)): + if index is not None and len(value) > index: + return int(value[index]) + elif index is None: + return value + else: + return int(value) + + return default + def _get_child_height(self, child: Renderable) -> int: """ Get the height of a child object. @@ -200,20 +240,15 @@ class Page(Renderable, Queriable): Returns: Height in pixels """ - if hasattr(child, '_size') and child._size is not None: - if isinstance( - child._size, (list, tuple, np.ndarray)) and len( - child._size) >= 2: - return int(child._size[1]) + # Try to get height from size property (index 1) + height = self._get_child_property(child, '_size', 'size', index=1) + if height is not None: + return height - if hasattr(child, 'size') and child.size is not None: - if isinstance( - child.size, (list, tuple, np.ndarray)) and len( - child.size) >= 2: - return int(child.size[1]) - - if hasattr(child, 'height'): - return int(child.height) + # Try direct height attribute + height = self._get_child_property(child, '_height', 'height') + if height is not None: + return height # Default fallback height return 20 @@ -286,19 +321,12 @@ class Page(Renderable, Queriable): Returns: Tuple of (x, y) coordinates """ - if hasattr(child, '_origin') and child._origin is not None: - if isinstance(child._origin, np.ndarray): - return (int(child._origin[0]), int(child._origin[1])) - elif isinstance(child._origin, (list, tuple)) and len(child._origin) >= 2: - return (int(child._origin[0]), int(child._origin[1])) + # Try to get x coordinate + x = self._get_child_property(child, '_origin', 'position', index=0, default=0) + # Try to get y coordinate + y = self._get_child_property(child, '_origin', 'position', index=1, default=0) - if hasattr(child, 'position'): - pos = child.position - if isinstance(pos, (list, tuple)) and len(pos) >= 2: - return (int(pos[0]), int(pos[1])) - - # Default to origin - return (0, 0) + return (x, y) def query_point(self, point: Tuple[int, int]) -> Optional[QueryResult]: """ @@ -377,20 +405,20 @@ class Page(Renderable, Queriable): Returns: Tuple of (width, height) or None if size cannot be determined """ - if hasattr(child, '_size') and child._size is not None: - if isinstance( - child._size, (list, tuple, np.ndarray)) and len( - child._size) >= 2: - return (int(child._size[0]), int(child._size[1])) + # Try to get width and height from size property + width = self._get_child_property(child, '_size', 'size', index=0) + height = self._get_child_property(child, '_size', 'size', index=1) - if hasattr(child, 'size') and child.size is not None: - if isinstance( - child.size, (list, tuple, np.ndarray)) and len( - child.size) >= 2: - return (int(child.size[0]), int(child.size[1])) + # If size property worked, return it + if width is not None and height is not None: + return (width, height) - if hasattr(child, 'width') and hasattr(child, 'height'): - return (int(child.width), int(child.height)) + # Try direct width/height attributes + width = self._get_child_property(child, '_width', 'width') + height = self._get_child_property(child, '_height', 'height') + + if width is not None and height is not None: + return (width, height) return None diff --git a/pyWebLayout/core/base.py b/pyWebLayout/core/base.py index 28cd681..79ececc 100644 --- a/pyWebLayout/core/base.py +++ b/pyWebLayout/core/base.py @@ -331,10 +331,16 @@ class BlockContainer: super().__init__(*args, **kwargs) self._blocks = [] - @property def blocks(self): - """Get the list of blocks in this container""" - return self._blocks + """ + Get an iterator over the blocks in this container. + + Can be used as blocks() for iteration or accessing the _blocks list directly. + + Returns: + Iterator over blocks + """ + return iter(self._blocks) def add_block(self, block): """ diff --git a/pyWebLayout/style/fonts.py b/pyWebLayout/style/fonts.py index 3831ee5..194baf0 100644 --- a/pyWebLayout/style/fonts.py +++ b/pyWebLayout/style/fonts.py @@ -359,62 +359,48 @@ class Font: """Get the minimum width required for hyphenation to be considered""" return self._min_hyphenation_width + def _with_modified(self, **kwargs): + """ + Internal helper to create a new Font with modified parameters. + + This consolidates the duplication across all with_* methods. + + Args: + **kwargs: Parameters to override (e.g., font_size=20, colour=(255,0,0)) + + Returns: + New Font object with modified parameters + """ + params = { + 'font_path': self._font_path, + 'font_size': self._font_size, + 'colour': self._colour, + 'weight': self._weight, + 'style': self._style, + 'decoration': self._decoration, + 'background': self._background, + 'language': self.language, + 'min_hyphenation_width': self._min_hyphenation_width + } + params.update(kwargs) + return Font(**params) + def with_size(self, size: int): """Create a new Font object with modified size""" - return Font( - self._font_path, - size, - self._colour, - self._weight, - self._style, - self._decoration, - self._background - ) + return self._with_modified(font_size=size) def with_colour(self, colour: Tuple[int, int, int]): """Create a new Font object with modified colour""" - return Font( - self._font_path, - self._font_size, - colour, - self._weight, - self._style, - self._decoration, - self._background - ) + return self._with_modified(colour=colour) def with_weight(self, weight: FontWeight): """Create a new Font object with modified weight""" - return Font( - self._font_path, - self._font_size, - self._colour, - weight, - self._style, - self._decoration, - self._background - ) + return self._with_modified(weight=weight) def with_style(self, style: FontStyle): """Create a new Font object with modified style""" - return Font( - self._font_path, - self._font_size, - self._colour, - self._weight, - style, - self._decoration, - self._background - ) + return self._with_modified(style=style) def with_decoration(self, decoration: TextDecoration): """Create a new Font object with modified decoration""" - return Font( - self._font_path, - self._font_size, - self._colour, - self._weight, - self._style, - decoration, - self._background - ) + return self._with_modified(decoration=decoration)