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)