diff --git a/coverage-docs.svg b/coverage-docs.svg
index 73800a2..2d6395b 100644
--- a/coverage-docs.svg
+++ b/coverage-docs.svg
@@ -1,5 +1,5 @@
diff --git a/coverage.xml b/coverage.xml
index 3959650..b826073 100644
--- a/coverage.xml
+++ b/coverage.xml
@@ -1,5 +1,5 @@
-
+
@@ -652,7 +652,7 @@
-
+
@@ -665,18 +665,18 @@
-
+
+
+
+
-
-
-
@@ -684,12 +684,14 @@
+
+
-
-
-
-
-
+
+
+
+
+
@@ -697,253 +699,253 @@
+
+
-
-
-
-
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
-
-
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
-
-
-
-
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
@@ -951,71 +953,71 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
@@ -1023,50 +1025,50 @@
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -1089,32 +1091,99 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2356,7 +2425,7 @@
-
+
@@ -2872,7 +2941,7 @@
-
+
@@ -2907,9 +2976,9 @@
-
-
-
+
+
+
@@ -3194,21 +3263,21 @@
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
diff --git a/pyWebLayout/__init__.py b/pyWebLayout/__init__.py
index 7df731a..dd4611e 100644
--- a/pyWebLayout/__init__.py
+++ b/pyWebLayout/__init__.py
@@ -34,8 +34,6 @@ from pyWebLayout.concrete.page import Container, Page
# Abstract components
from pyWebLayout.abstract.inline import Word
-# Layout components
-from pyWebLayout.table import Table, TableCell
# IO functionality (reading and writing)
from pyWebLayout.io import (
diff --git a/pyWebLayout/abstract/functional.py b/pyWebLayout/abstract/functional.py
index fa120f3..4fa47cc 100644
--- a/pyWebLayout/abstract/functional.py
+++ b/pyWebLayout/abstract/functional.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from enum import Enum
from typing import Callable, Dict, Any, Optional, Union, List, Tuple
-from pyWebLayout.base import Interactable
+from pyWebLayout.core.base import Interactable
class LinkType(Enum):
diff --git a/pyWebLayout/abstract/inline.py b/pyWebLayout/abstract/inline.py
index e56b96a..209b0ee 100644
--- a/pyWebLayout/abstract/inline.py
+++ b/pyWebLayout/abstract/inline.py
@@ -1,5 +1,5 @@
from __future__ import annotations
-from pyWebLayout.base import Queriable
+from pyWebLayout.core.base import Queriable
from pyWebLayout.style import Font
from typing import Tuple, Union, List, Optional, Dict
import pyphen
diff --git a/pyWebLayout/base.py b/pyWebLayout/base.py
deleted file mode 100644
index cad1eea..0000000
--- a/pyWebLayout/base.py
+++ /dev/null
@@ -1,68 +0,0 @@
-
-from abc import ABC
-import numpy as np
-
-from pyWebLayout.style import Alignment
-
-
-class Renderable(ABC):
- """
- Abstract base class for any object that can be rendered to an image.
- All renderable objects must implement the render method.
- """
- def render(self):
- """
- Render the object to an image.
-
- Returns:
- PIL.Image: The rendered image
- """
- pass
-
-class Interactable(ABC):
- """
- Abstract base class for any object that can be interacted with.
- Interactable objects must have a callback that is executed when interacted with.
- """
- def __init__(self, callback=None):
- """
- Initialize an interactable object.
-
- Args:
- callback: The function to call when this object is interacted with
- """
- self._callback = callback
-
- def interact(self, point: np.generic):
- """
- Handle interaction at the given point.
-
- Args:
- point: The coordinates of the interaction
-
- Returns:
- The result of calling the callback function with the point
- """
- if self._callback is None:
- return None
- return self._callback(point)
-
-class Layoutable(ABC):
- """
- Abstract base class for any object that can be laid out.
- Layoutable objects must implement the layout method which arranges their contents.
- """
- def layout(self):
- """
- Layout the object's contents.
- This method should be called before rendering to properly arrange the object's contents.
- """
- pass
-
-class Queriable(ABC):
-
- def in_object(self, point:np.generic):
- """
- check if a point is in the object
- """
- pass
diff --git a/pyWebLayout/concrete/box.py b/pyWebLayout/concrete/box.py
index 588c1e5..8827945 100644
--- a/pyWebLayout/concrete/box.py
+++ b/pyWebLayout/concrete/box.py
@@ -1,8 +1,8 @@
import numpy as np
from PIL import Image
-from pyWebLayout.base import Renderable, Queriable
-from pyWebLayout.layout import Alignment
+from pyWebLayout.core.base import Renderable, Queriable
+from pyWebLayout.style.layout import Alignment
class Box(Renderable, Queriable):
diff --git a/pyWebLayout/concrete/functional.py b/pyWebLayout/concrete/functional.py
index 62544d9..89b7907 100644
--- a/pyWebLayout/concrete/functional.py
+++ b/pyWebLayout/concrete/functional.py
@@ -3,7 +3,7 @@ from typing import Optional, Dict, Any, Tuple, List, Union
import numpy as np
from PIL import Image, ImageDraw, ImageFont
-from pyWebLayout.base import Renderable, Queriable
+from pyWebLayout.core.base import Renderable, Queriable
from pyWebLayout.abstract.functional import Link, Button, Form, FormField, LinkType, FormFieldType
from pyWebLayout.style import Font, TextDecoration
from .box import Box
diff --git a/pyWebLayout/concrete/image.py b/pyWebLayout/concrete/image.py
index b388f7a..95515c4 100644
--- a/pyWebLayout/concrete/image.py
+++ b/pyWebLayout/concrete/image.py
@@ -3,10 +3,10 @@ from typing import Optional, Tuple, Union, Dict, Any
import numpy as np
from PIL import Image as PILImage, ImageDraw, ImageFont
-from pyWebLayout.base import Renderable, Queriable
+from pyWebLayout.core.base import Renderable, Queriable
from pyWebLayout.abstract.block import Image as AbstractImage
from .box import Box
-from pyWebLayout.layout import Alignment
+from pyWebLayout.style.layout import Alignment
class RenderableImage(Box, Queriable):
diff --git a/pyWebLayout/concrete/page.py b/pyWebLayout/concrete/page.py
index cedd028..74a9c70 100644
--- a/pyWebLayout/concrete/page.py
+++ b/pyWebLayout/concrete/page.py
@@ -2,9 +2,9 @@ from typing import List, Tuple, Optional, Dict, Any
import numpy as np
from PIL import Image
-from pyWebLayout.base import Renderable, Layoutable
+from pyWebLayout.core.base import Renderable, Layoutable
from .box import Box
-from pyWebLayout.layout import Alignment
+from pyWebLayout.style.layout import Alignment
class Container(Box, Layoutable):
diff --git a/pyWebLayout/concrete/text.py b/pyWebLayout/concrete/text.py
index fae1864..e399b49 100644
--- a/pyWebLayout/concrete/text.py
+++ b/pyWebLayout/concrete/text.py
@@ -1,7 +1,7 @@
from __future__ import annotations
-from pyWebLayout.base import Renderable, Queriable
+from pyWebLayout.core.base import Renderable, Queriable
from .box import Box
-from pyWebLayout.layout import Alignment
+from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
from pyWebLayout.abstract.inline import Word
from PIL import Image, ImageDraw, ImageFont
diff --git a/pyWebLayout/html_parser.py b/pyWebLayout/html_parser.py
deleted file mode 100644
index b622645..0000000
--- a/pyWebLayout/html_parser.py
+++ /dev/null
@@ -1,919 +0,0 @@
-import re
-from html.parser import HTMLParser as BaseHTMLParser
-from typing import Dict, List, Optional, Tuple, Union, Any, Set, Callable
-import urllib.parse
-from PIL import Image
-
-from .style import Font, FontStyle, FontWeight, TextDecoration
-from .abstract.document import Document, MetadataType, Book, Chapter
-from .abstract.block import (
- Block, BlockType, Paragraph, Heading, HeadingLevel, Quote, CodeBlock,
- HList, ListStyle, ListItem, Table, TableRow, TableCell, HorizontalRule
-)
-from .abstract.inline import Word, FormattedSpan, LineBreak
-from .abstract.functional import Link, LinkType, Button, Form, FormField, FormFieldType
-from .concrete.page import Page
-from pyWebLayout.layout import Alignment
-
-
-class HTMLParser(BaseHTMLParser):
- """
- HTML parser that builds an abstract document representation from HTML content.
- This parser converts HTML to abstract document classes without any rendering specifics.
- """
-
- def __init__(self, base_url: Optional[str] = None):
- """
- Initialize the HTML parser.
-
- Args:
- base_url: Base URL for resolving relative links
- """
- super().__init__()
-
- # Document structure
- self.document = Document()
-
- # State variables
- self._current_block = None
- self._block_stack: List[Block] = []
-
- # Text handling
- self._current_paragraph = None
- self._current_span = None
- self._text_buffer = ""
-
- # Style state
- self._style_stack: List[Dict[str, Any]] = []
- self._current_style = {
- 'font_size': 12,
- 'font_weight': FontWeight.NORMAL,
- 'font_style': FontStyle.NORMAL,
- 'decoration': TextDecoration.NONE,
- 'color': (0, 0, 0),
- 'background': None,
- 'language': 'en_US'
- }
-
- # Tag state
- self._list_stack: List[HList] = []
- self._table_stack: List[Table] = []
- self._current_table_row = None
-
- # Link handling
- self._base_url = base_url
- self._in_link = False
- self._current_link = None
-
- # Special state flags
- self._in_head = False
- self._in_title = False
- self._in_script = False
- self._in_style = False
- self._script_buffer = ""
- self._style_buffer = ""
- self._title_buffer = ""
-
- def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]):
- """
- Handle the start of an HTML tag.
-
- Args:
- tag: The tag name
- attrs: List of attribute tuples (name, value)
- """
- tag = tag.lower()
- attrs_dict = dict(attrs)
-
- # Special handling for elements where we collect content
- if self._in_script and tag != 'script':
- return
- if self._in_style and tag != 'style':
- return
-
- # Parse style attribute if present
- style = {}
- if 'style' in attrs_dict:
- style = self._parse_style(attrs_dict['style'])
-
- # Apply tag-specific styling based on the tag
- tag_style = self._get_tag_style(tag)
- for key, value in tag_style.items():
- if key not in style:
- style[key] = value
-
- # Push the current style and apply the new style
- self._push_style(style)
-
- # Handle specific tags
- if tag == 'html':
- # Set document language if specified
- if 'lang' in attrs_dict:
- self.document.set_metadata(MetadataType.LANGUAGE, attrs_dict['lang'])
-
- elif tag == 'head':
- self._in_head = True
-
- elif tag == 'title' and self._in_head:
- self._in_title = True
- self._title_buffer = ""
-
- elif tag == 'meta' and self._in_head:
- self._handle_meta_tag(attrs_dict)
-
- elif tag == 'link' and self._in_head:
- self._handle_link_tag(attrs_dict)
-
- elif tag == 'script':
- self._in_script = True
- self._script_buffer = ""
-
- elif tag == 'style':
- self._in_style = True
- self._style_buffer = ""
-
- elif tag == 'body':
- # Body attributes can contain style information
- pass
-
- elif tag == 'p':
- self._flush_text() # Flush any pending text
- self._current_paragraph = Paragraph()
-
- # Add the paragraph to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(self._current_paragraph)
- else:
- self.document.add_block(self._current_paragraph)
-
- # Push to block stack
- self._block_stack.append(self._current_paragraph)
- self._current_block = self._current_paragraph
-
- elif tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
- self._flush_text() # Flush any pending text
-
- # Determine heading level
- level_map = {
- 'h1': HeadingLevel.H1,
- 'h2': HeadingLevel.H2,
- 'h3': HeadingLevel.H3,
- 'h4': HeadingLevel.H4,
- 'h5': HeadingLevel.H5,
- 'h6': HeadingLevel.H6
- }
-
- heading = Heading(level=level_map[tag])
-
- # Add the heading to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(heading)
- else:
- self.document.add_block(heading)
-
- # Push to block stack
- self._block_stack.append(heading)
- self._current_block = heading
- self._current_paragraph = heading # Heading inherits from Paragraph
-
- elif tag == 'div':
- self._flush_text() # Flush any pending text
-
- # For divs, we create a new paragraph as a container
- div_para = Paragraph()
-
- # Add the div to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(div_para)
- else:
- self.document.add_block(div_para)
-
- # Push to block stack
- self._block_stack.append(div_para)
- self._current_block = div_para
- self._current_paragraph = div_para
-
- elif tag == 'blockquote':
- self._flush_text() # Flush any pending text
-
- quote = Quote()
-
- # Add the quote to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(quote)
- else:
- self.document.add_block(quote)
-
- # Push to block stack
- self._block_stack.append(quote)
- self._current_block = quote
-
- elif tag == 'pre':
- self._flush_text() # Flush any pending text
-
- # Pre can optionally contain a code block
- # We'll create a paragraph for now, and if we find a code tag inside,
- # we'll replace it with a code block
- pre_para = Paragraph()
-
- # Add the pre to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(pre_para)
- else:
- self.document.add_block(pre_para)
-
- # Push to block stack
- self._block_stack.append(pre_para)
- self._current_block = pre_para
- self._current_paragraph = pre_para
-
- elif tag == 'code':
- # If we're inside a pre, replace the paragraph with a code block
- if self._block_stack and isinstance(self._block_stack[-1], Paragraph):
- pre_para = self._block_stack.pop()
-
- # Get the language from class if specified (e.g., class="language-python")
- language = ""
- if 'class' in attrs_dict:
- class_attr = attrs_dict['class']
- if class_attr.startswith('language-'):
- language = class_attr[9:]
-
- code_block = CodeBlock(language=language)
-
- # Replace the paragraph with the code block
- if pre_para.parent:
- parent = pre_para.parent
- if hasattr(parent, '_blocks'):
- # Find the paragraph in the parent's blocks and replace it
- for i, block in enumerate(parent._blocks):
- if block == pre_para:
- parent._blocks[i] = code_block
- break
-
- # Push the code block to the stack
- self._block_stack.append(code_block)
- self._current_block = code_block
- self._current_paragraph = None
- else:
- # If not in a pre, just create a formatted span for code
- self._current_span = None # Force creation of a new span with code style
-
- elif tag in ('ul', 'ol', 'dl'):
- self._flush_text() # Flush any pending text
-
- # Determine list style
- style_map = {
- 'ul': ListStyle.UNORDERED,
- 'ol': ListStyle.ORDERED,
- 'dl': ListStyle.DEFINITION
- }
-
- list_block = HList(style=style_map[tag])
-
- # Add the list to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(list_block)
- else:
- self.document.add_block(list_block)
-
- # Push to block stack and list stack
- self._block_stack.append(list_block)
- self._list_stack.append(list_block)
- self._current_block = list_block
- self._current_paragraph = None
-
- elif tag == 'li' and self._list_stack:
- self._flush_text() # Flush any pending text
-
- list_item = ListItem()
-
- # Add to the current list
- current_list = self._list_stack[-1]
- current_list.add_item(list_item)
-
- # Push to block stack
- self._block_stack.append(list_item)
- self._current_block = list_item
- self._current_paragraph = None
-
- elif tag == 'dt' and self._list_stack and self._list_stack[-1].style == ListStyle.DEFINITION:
- self._flush_text() # Flush any pending text
-
- # For definition term, we create a list item with a term
- list_item = ListItem(term="") # Will be filled by content
-
- # Add to the current list
- current_list = self._list_stack[-1]
- current_list.add_item(list_item)
-
- # Push to block stack
- self._block_stack.append(list_item)
- self._current_block = list_item
-
- # Create a paragraph for the term content
- term_para = Paragraph()
- list_item.add_block(term_para)
- self._current_paragraph = term_para
-
- elif tag == 'dd' and self._list_stack and self._list_stack[-1].style == ListStyle.DEFINITION:
- self._flush_text() # Flush any pending text
-
- # Find the last dt item
- current_list = self._list_stack[-1]
- if current_list._items:
- list_item = current_list._items[-1]
-
- # Create a paragraph for the description content
- desc_para = Paragraph()
- list_item.add_block(desc_para)
-
- # Update current state
- self._current_paragraph = desc_para
- else:
- # If no dt found, create a new list item
- list_item = ListItem()
- current_list.add_item(list_item)
-
- # Push to block stack
- self._block_stack.append(list_item)
- self._current_block = list_item
-
- # Create a paragraph for the description content
- desc_para = Paragraph()
- list_item.add_block(desc_para)
- self._current_paragraph = desc_para
-
- elif tag == 'table':
- self._flush_text() # Flush any pending text
-
- # Create a new table
- caption = None
- if 'summary' in attrs_dict:
- caption = attrs_dict['summary']
-
- table = Table(caption=caption)
-
- # Add the table to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(table)
- else:
- self.document.add_block(table)
-
- # Push to block stack and table stack
- self._block_stack.append(table)
- self._table_stack.append(table)
- self._current_block = table
- self._current_paragraph = None
-
- elif tag in ('thead', 'tbody', 'tfoot') and self._table_stack:
- # Just track the current section - no need to create new objects
- self._current_table_section = tag
-
- elif tag == 'tr' and self._table_stack:
- self._flush_text() # Flush any pending text
-
- # Create a new row
- row = TableRow()
-
- # Add to the current table
- current_table = self._table_stack[-1]
-
- # Determine the section based on context
- section = "body"
- if hasattr(self, '_current_table_section'):
- if self._current_table_section == 'thead':
- section = "header"
- elif self._current_table_section == 'tfoot':
- section = "footer"
-
- current_table.add_row(row, section=section)
-
- # Update state
- self._current_table_row = row
- self._current_paragraph = None
-
- elif tag in ('td', 'th') and self._current_table_row:
- self._flush_text() # Flush any pending text
-
- # Parse attributes
- colspan = 1
- rowspan = 1
-
- if 'colspan' in attrs_dict:
- try:
- colspan = int(attrs_dict['colspan'])
- except (ValueError, TypeError):
- pass
-
- if 'rowspan' in attrs_dict:
- try:
- rowspan = int(attrs_dict['rowspan'])
- except (ValueError, TypeError):
- pass
-
- # Create a new cell
- is_header = (tag == 'th')
- cell = TableCell(is_header=is_header, colspan=colspan, rowspan=rowspan)
-
- # Add to the current row
- self._current_table_row.add_cell(cell)
-
- # Push to block stack
- self._block_stack.append(cell)
- self._current_block = cell
-
- # Create a paragraph for the cell content
- cell_para = Paragraph()
- cell.add_block(cell_para)
- self._current_paragraph = cell_para
-
- elif tag == 'a':
- self._flush_text() # Flush any pending text
-
- # Parse attributes
- href = attrs_dict.get('href', '')
- title = attrs_dict.get('title', '')
-
- # Determine link type
- link_type = LinkType.INTERNAL
- if href.startswith('http://') or href.startswith('https://'):
- link_type = LinkType.EXTERNAL
- elif href.startswith('javascript:'):
- link_type = LinkType.FUNCTION
- elif href.startswith('api:'):
- link_type = LinkType.API
- href = href[4:] # Remove api: prefix
-
- # If we have a base URL and the href is relative, resolve it
- if self._base_url and not href.startswith(('http://', 'https://', 'javascript:', 'api:', '#')):
- href = urllib.parse.urljoin(self._base_url, href)
-
- # Create a Link object
- self._current_link = Link(
- location=href,
- link_type=link_type,
- title=title if title else None
- )
-
- # Set the flag to indicate we're inside a link
- self._in_link = True
-
- # Force creation of a new span with link style
- self._current_span = None
-
- elif tag == 'img':
- # Handle image
- src = attrs_dict.get('src', '')
- alt = attrs_dict.get('alt', '')
-
- # Parse width and height if provided
- width = None
- height = None
- if 'width' in attrs_dict:
- try:
- width = int(attrs_dict['width'])
- except (ValueError, TypeError):
- pass
-
- if 'height' in attrs_dict:
- try:
- height = int(attrs_dict['height'])
- except (ValueError, TypeError):
- pass
-
- # If we have a base URL and the src is relative, resolve it
- if self._base_url and not src.startswith(('http://', 'https://')):
- src = urllib.parse.urljoin(self._base_url, src)
-
- # Create an Image block
- from .abstract.block import Image
- image = Image(source=src, alt_text=alt, width=width, height=height)
-
- # Add the image to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(image)
- else:
- self.document.add_block(image)
-
- # Also add as a resource for backwards compatibility
- resource_name = f"img_{len(self.document._resources) + 1}"
- self.document.add_resource(resource_name, {
- 'type': 'image',
- 'src': src,
- 'alt': alt,
- 'width': width,
- 'height': height,
- 'image_object': image
- })
-
- elif tag == 'br':
-
- # Add a line break
- if self._current_paragraph:
- line_break = LineBreak()
- if hasattr(self._current_paragraph, 'add_block'):
- self._current_paragraph.add_block(line_break)
-
- # Flush any text before the break
- self._flush_text()
-
- elif tag == 'hr':
- self._flush_text() # Flush any pending text
-
- # Create a horizontal rule
- hr = HorizontalRule()
-
- # Add to the current block or document
- if self._current_block and hasattr(self._current_block, 'add_block'):
- self._current_block.add_block(hr)
- else:
- self.document.add_block(hr)
-
- elif tag in ('b', 'strong'):
- # Bold text
- self._current_style['font_weight'] = FontWeight.BOLD
- self._current_span = None # Force creation of a new span
-
- elif tag in ('i', 'em'):
- # Italic text
- self._current_style['font_style'] = FontStyle.ITALIC
- self._current_span = None # Force creation of a new span
-
- elif tag == 'u':
- # Underlined text
- self._current_style['decoration'] = TextDecoration.UNDERLINE
- self._current_span = None # Force creation of a new span
-
- elif tag == 'span':
- # Span can have style attributes
- self._current_span = None # Force creation of a new span
-
- elif tag == 'form':
- self._flush_text() # Flush any pending text
-
- # Parse attributes
- form_id = attrs_dict.get('id', f"form_{len(self.document._resources) + 1}")
- action = attrs_dict.get('action', '')
-
- # Create a Form object
- form = Form(form_id=form_id, action=action)
-
- # Add as a resource
- self.document.add_resource(form_id, form)
-
- # TODO: Create a proper Form block class and add it to the document
-
- elif tag == 'input':
- # Parse attributes
- input_type = attrs_dict.get('type', 'text')
- input_name = attrs_dict.get('name', '')
- input_value = attrs_dict.get('value', '')
- input_required = 'required' in attrs_dict
-
- # Map HTML input types to FormFieldType
- type_map = {
- 'text': FormFieldType.TEXT,
- 'password': FormFieldType.PASSWORD,
- 'checkbox': FormFieldType.CHECKBOX,
- 'radio': FormFieldType.RADIO,
- 'number': FormFieldType.NUMBER,
- 'date': FormFieldType.DATE,
- 'time': FormFieldType.TIME,
- 'email': FormFieldType.EMAIL,
- 'url': FormFieldType.URL,
- 'color': FormFieldType.COLOR,
- 'range': FormFieldType.RANGE,
- 'hidden': FormFieldType.HIDDEN
- }
-
- field_type = type_map.get(input_type, FormFieldType.TEXT)
-
- # Create a FormField object
- field = FormField(
- name=input_name,
- field_type=field_type,
- label=attrs_dict.get('placeholder', input_name),
- value=input_value,
- required=input_required
- )
-
- # TODO: Add the field to a form if inside a form
-
- elif tag == 'textarea':
- # Similar to input but with multiline content
- # We'll handle the content in handle_data
- pass
-
- elif tag == 'select':
- # Similar to input but with options
- # We'll handle the options in handle_data
- pass
-
- elif tag == 'button':
- # Parse attributes
- button_type = attrs_dict.get('type', 'button')
- button_name = attrs_dict.get('name', '')
-
- # TODO: Create a Button object and add it to the document
-
- def handle_endtag(self, tag: str):
- """
- Handle the end of an HTML tag.
-
- Args:
- tag: The tag name
- """
- tag = tag.lower()
-
- # Special handling for elements where we collect content
- if tag == 'script' and self._in_script:
- self._in_script = False
- self.document.add_script(self._script_buffer)
- self._script_buffer = ""
- self._pop_style()
- return
-
- if tag == 'style' and self._in_style:
- self._in_style = False
- # Parse the style and add to document
- stylesheet = self._parse_css(self._style_buffer)
- if stylesheet:
- self.document.add_stylesheet(stylesheet)
- self._style_buffer = ""
- self._pop_style()
- return
-
- if tag == 'title' and self._in_title:
- self._in_title = False
- self.document.set_title(self._title_buffer.strip())
- self._title_buffer = ""
- self._pop_style()
- return
-
- if self._in_script and tag != 'script':
- return
- if self._in_style and tag != 'style':
- return
-
- # Flush any accumulated text
- self._flush_text()
-
- # Handle specific end tags
- if tag == 'head':
- self._in_head = False
-
- elif tag == 'body':
- pass # Nothing special to do
-
- elif tag in ('p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'):
- # Pop from block stack
- if self._block_stack:
- self._block_stack.pop()
-
- # Update current block
- if self._block_stack:
- self._current_block = self._block_stack[-1]
- else:
- self._current_block = None
-
- # Reset current paragraph
- self._current_paragraph = None
- self._current_span = None
-
- elif tag == 'code':
- # If we're inside a code block, no need to do anything special
- pass
-
- elif tag in ('ul', 'ol', 'dl'):
- # Pop from block stack and list stack
- if self._block_stack:
- self._block_stack.pop()
-
- if self._list_stack:
- self._list_stack.pop()
-
- # Update current block
- if self._block_stack:
- self._current_block = self._block_stack[-1]
- else:
- self._current_block = None
-
- # Reset current paragraph
- self._current_paragraph = None
- self._current_span = None
-
- elif tag in ('li', 'dt', 'dd'):
- # Pop from block stack
- if self._block_stack:
- self._block_stack.pop()
-
- # Update current block
- if self._block_stack:
- self._current_block = self._block_stack[-1]
- else:
- self._current_block = None
-
- # Reset current paragraph
- self._current_paragraph = None
- self._current_span = None
-
- elif tag == 'table':
- # Pop from block stack and table stack
- if self._block_stack:
- self._block_stack.pop()
-
- if self._table_stack:
- self._table_stack.pop()
-
- # Update current block
- if self._block_stack:
- self._current_block = self._block_stack[-1]
- else:
- self._current_block = None
-
- # Reset current paragraph and table state
- self._current_paragraph = None
- self._current_span = None
- self._current_table_row = None
- if hasattr(self, '_current_table_section'):
- delattr(self, '_current_table_section')
-
- elif tag in ('thead', 'tbody', 'tfoot'):
- # Clear current section
- if hasattr(self, '_current_table_section'):
- delattr(self, '_current_table_section')
-
- elif tag == 'tr':
- # Reset current row
- self._current_table_row = None
-
- elif tag in ('td', 'th'):
- # Pop from block stack
- if self._block_stack:
- self._block_stack.pop()
-
- # Update current block
- if self._block_stack:
- self._current_block = self._block_stack[-1]
- else:
- self._current_block = None
-
- # Reset current paragraph
- self._current_paragraph = None
- self._current_span = None
-
- elif tag == 'a':
- # End of link
- self._in_link = False
- self._current_link = None
-
- elif tag in ('b', 'strong', 'i', 'em', 'u', 'span'):
- # End of styled text
- self._current_span = None
-
- # Pop style regardless of tag
- self._pop_style()
-
- def handle_data(self, data: str):
- """
- Handle text data.
-
- Args:
- data: The text data
- """
- if self._in_script:
- self._script_buffer += data
- return
-
- if self._in_style:
- self._style_buffer += data
- return
-
- if self._in_title:
- self._title_buffer += data
- return
-
- # Add to text buffer
- self._text_buffer += data
-
- def handle_entityref(self, name: str):
- """
- Handle an HTML entity reference.
-
- Args:
- name: The entity name
- """
- # Map common entity references to characters
- entities = {
- 'lt': '<',
- 'gt': '>',
- 'amp': '&',
- 'quot': '"',
- 'apos': "'",
- 'nbsp': ' ',
- 'copy': '©',
- 'reg': '®',
- 'trade': '™',
- }
-
- if name in entities:
- char = entities[name]
- else:
- try:
- import html.entities
- char = chr(html.entities.name2codepoint[name])
- except (KeyError, ImportError):
- char = f'&{name};'
-
- # Handle based on context
- if self._in_script:
- self._script_buffer += char
- elif self._in_style:
- self._style_buffer += char
- elif self._in_title:
- self._title_buffer += char
- else:
- self._text_buffer += char
-
- def handle_charref(self, name: str):
- """
- Handle a character reference.
-
- Args:
- name: The character reference (decimal or hex)
- """
- # Convert character reference to character
- if name.startswith('x'):
- # Hexadecimal reference
- char = chr(int(name[1:], 16))
- else:
- # Decimal reference
- char = chr(int(name))
-
- # Handle based on context
- if self._in_script:
- self._script_buffer += char
- elif self._in_style:
- self._style_buffer += char
- elif self._in_title:
- self._title_buffer += char
- else:
- self._text_buffer += char
-
- def _push_style(self, style: Dict[str, Any]):
- """
- Push a new style onto the style stack.
-
- Args:
- style: The style to push
- """
- # Save the current style
- self._style_stack.append(self._current_style.copy())
-
- # Apply the new style
- for key, value in style.items():
- self._current_style[key] = value
-
- def _pop_style(self):
- """Pop a style from the style stack."""
- if self._style_stack:
- self._current_style = self._style_stack.pop()
-
- def _get_tag_style(self, tag: str) -> Dict[str, Any]:
- """
- Get the default style for a tag.
-
- Args:
- tag: The tag name
-
- Returns:
- A dictionary of style properties
- """
- # Default styles for common tags
- tag_styles = {
- 'h1': {'font_size': 24, 'font_weight': FontWeight.BOLD},
- 'h2': {'font_size': 20, 'font_weight': FontWeight.BOLD},
- 'h3': {'font_size': 18, 'font_weight': FontWeight.BOLD},
- 'h4': {'font_size': 16, 'font_weight': FontWeight.BOLD},
- 'h5': {'font_size': 14, 'font_weight': FontWeight.BOLD},
- 'h6': {'font_size': 12, 'font_weight': FontWeight.BOLD},
- 'b': {'font_weight': FontWeight.BOLD},
- 'strong': {'font_weight': FontWeight.BOLD},
- 'i': {'font_style': FontStyle.ITALIC},
- 'em': {'font_style': FontStyle.ITALIC},
- 'u': {'decoration': TextDecoration.UNDERLINE},
- 'a': {'decoration': TextDecoration.UNDERLINE, 'color': (0, 0, 255)},
- 'code': {'font_family': 'monospace', 'background': (240, 240, 240, 255)},
- 'pre': {'font_family': 'monospace'},
- }
-
- return tag_styles.get(tag, {})
-
- def _create_font(self) -> Font:
- """
- Create a Font object from the current style.
-
- Returns:
- Font: A font object with the current style settings
- """
diff --git a/pyWebLayout/localisation.py b/pyWebLayout/localisation.py
deleted file mode 100644
index 90dae17..0000000
--- a/pyWebLayout/localisation.py
+++ /dev/null
@@ -1 +0,0 @@
-## list langauges
\ No newline at end of file
diff --git a/pyWebLayout/style.py b/pyWebLayout/style.py
deleted file mode 100644
index 819d2b5..0000000
--- a/pyWebLayout/style.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# this should contain classes for how different object can be rendered, e.g. bold, italic, regular
-from PIL import ImageFont
-from enum import Enum
-from typing import Tuple, Union, Optional
-
-
-class FontWeight(Enum):
- NORMAL = "normal"
- BOLD = "bold"
-
-
-class FontStyle(Enum):
- NORMAL = "normal"
- ITALIC = "italic"
-
-
-class TextDecoration(Enum):
- NONE = "none"
- UNDERLINE = "underline"
- STRIKETHROUGH = "strikethrough"
-
-
-class Font:
- """
- Font class to manage text rendering properties including font face, size, color, and styling.
- This class is used by the text renderer to determine how to render text.
- """
-
- def __init__(self,
- font_path: Optional[str] = None,
- font_size: int = 12,
- 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 = "en_EN"):
- """
- Initialize a Font object 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.
- """
- self._font_path = font_path
- self._font_size = font_size
- self._colour = colour
- self._weight = weight
- self._style = style
- self._decoration = decoration
- self._background = background if background else (255, 255, 255, 0)
- self.language = language
- # Load the font file or use default
- self._load_font()
-
- def _load_font(self):
- """Load the font using PIL's ImageFont"""
- try:
- if self._font_path:
- self._font = ImageFont.truetype(
- self._font_path,
- self._font_size
- )
- else:
- # Use default font
- self._font = ImageFont.load_default()
- if self._font_size != 12: # Default size might not be 12
- self._font = ImageFont.truetype(self._font.path, self._font_size)
- except Exception as e:
- print(f"Error loading font: {e}")
- self._font = ImageFont.load_default()
-
- @property
- def font(self):
- """Get the PIL ImageFont object"""
- return self._font
-
- @property
- def font_size(self):
- """Get the font size"""
- return self._font_size
-
- @property
- def colour(self):
- """Get the text color"""
- return self._colour
-
- @property
- def color(self):
- """Alias for colour (American spelling)"""
- return self._colour
-
- @property
- def background(self):
- """Get the background color"""
- return self._background
-
- @property
- def weight(self):
- """Get the font weight"""
- return self._weight
-
- @property
- def style(self):
- """Get the font style"""
- return self._style
-
- @property
- def decoration(self):
- """Get the text decoration"""
- return self._decoration
-
- 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
- )
-
- 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
- )
-
- 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
- )
-
- 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
- )
-
- 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
- )
diff --git a/pyWebLayout/layout.py b/pyWebLayout/style/layout.py
similarity index 100%
rename from pyWebLayout/layout.py
rename to pyWebLayout/style/layout.py
diff --git a/pyWebLayout/table.py b/pyWebLayout/table.py
deleted file mode 100644
index 81b4999..0000000
--- a/pyWebLayout/table.py
+++ /dev/null
@@ -1,137 +0,0 @@
-from pyWebLayout.base import Renderable
-from .concrete.box import Box
-from pyWebLayout.layout import Alignment
-
-import numpy as np
-from PIL import Image, ImageDraw
-from typing import List, Tuple, Optional
-
-
-class TableCell(Box):
- def __init__(self, origin, size, content: Optional[Renderable] = None,
- callback=None, sheet=None, mode=None,
- halign=Alignment.CENTER, valign=Alignment.CENTER,
- padding: Tuple[int, int, int, int] = (5, 5, 5, 5)):
- """
- Initialize a table cell.
-
- Args:
- origin: Top-left corner coordinates
- size: Width and height of the cell
- content: Optional renderable content to place in the cell
- callback: Optional callback function
- sheet: Optional image sheet
- mode: Optional image mode
- halign: Horizontal alignment
- valign: Vertical alignment
- padding: Padding as (top, right, bottom, left)
- """
- super().__init__(origin, size, callback, sheet, mode, halign, valign)
- self._content = content
- self._padding = padding # (top, right, bottom, left)
-
- def set_content(self, content: Renderable):
- """Set the content of this cell"""
- self._content = content
-
- def render(self) -> Image:
- """Render the cell with its content and border"""
- # Create the base canvas
- canvas = super().render()
- draw = ImageDraw.Draw(canvas)
-
- # Draw border (optional - can be customized)
- draw.rectangle([(0, 0), tuple(self._size - np.array([1, 1]))],
- outline=(0, 0, 0), width=1)
-
- return canvas
-
-
-class Table(Box):
- def __init__(self, rows: int, columns: int, origin, size,
- cell_padding: Tuple[int, int, int, int] = (5, 5, 5, 5),
- callback=None, sheet=None, mode=None,
- halign=Alignment.CENTER, valign=Alignment.CENTER):
- """
- Initialize a table with specified number of rows and columns.
-
- Args:
- rows: Number of rows in the table
- columns: Number of columns in the table
- origin: Top-left corner coordinates
- size: Width and height of the table
- cell_padding: Padding for each cell as (top, right, bottom, left)
- callback: Optional callback function
- sheet: Optional image sheet
- mode: Optional image mode
- halign: Horizontal alignment
- valign: Vertical alignment
- """
- super().__init__(origin, size, callback, sheet, mode, halign, valign)
-
- self._rows = rows
- self._columns = columns
- self._cell_padding = cell_padding
-
- # Calculate cell dimensions
- cell_width = size[0] // columns
- cell_height = size[1] // rows
-
- # Create a 2D array of cells
- self._cells: List[List[TableCell]] = []
-
- for row in range(rows):
- cell_row = []
- for col in range(columns):
- # Calculate cell position
- cell_origin = np.array([col * cell_width, row * cell_height])
- cell_size = np.array([cell_width, cell_height])
-
- # Create the cell
- cell = TableCell(
- origin=cell_origin,
- size=cell_size,
- sheet=sheet,
- mode=mode,
- halign=halign,
- valign=valign,
- padding=cell_padding
- )
-
- cell_row.append(cell)
-
- self._cells.append(cell_row)
-
- def add_to_cell(self, x: int, y: int, content: Renderable):
- """
- Add content to a specific cell in the table.
-
- Args:
- x: Column index (0-based)
- y: Row index (0-based)
- content: Renderable content to add to the cell
- """
- if 0 <= y < self._rows and 0 <= x < self._columns:
- self._cells[y][x].set_content(content)
- else:
- raise IndexError(f"Cell indices ({x}, {y}) out of range. Table is {self._columns}x{self._rows}")
-
- def render(self) -> Image:
- """Render the complete table with all cells"""
- # Create base canvas
- canvas = super().render()
-
- # Render each cell and paste it onto the canvas
- for row in range(self._rows):
- for col in range(self._columns):
- cell = self._cells[row][col]
- cell_img = cell.render()
-
- # Get the position for this cell
- cell_pos = (col * (self._size[0] // self._columns),
- row * (self._size[1] // self._rows))
-
- # Paste the cell onto the canvas
- canvas.paste(cell_img, cell_pos, cell_img)
-
- return canvas
diff --git a/pyproject.toml b/pyproject.toml
index 0568bd1..fbc2f43 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,6 +17,7 @@ dependencies = [
"numpy",
"pyphen",
"beautifulsoup4",
+ "flask"
]
[tool.coverage.run]