425 lines
14 KiB
Python
425 lines
14 KiB
Python
from __future__ import annotations
|
|
from pyWebLayout.core import Hierarchical
|
|
from pyWebLayout.style import Font
|
|
from pyWebLayout.style.abstract_style import AbstractStyle
|
|
from typing import Tuple, Union, List, Optional, Dict, Any, Callable
|
|
import pyphen
|
|
|
|
# Import LinkType for type hints (imported at module level to avoid F821 linting error)
|
|
from pyWebLayout.abstract.functional import LinkType
|
|
|
|
|
|
class Word:
|
|
"""
|
|
An abstract representation of a word in a document. Words can be split across
|
|
lines or pages during rendering. This class manages the logical representation
|
|
of a word without any rendering specifics.
|
|
|
|
Now uses AbstractStyle objects for memory efficiency and proper style management.
|
|
"""
|
|
|
|
def __init__(self,
|
|
text: str,
|
|
style: Union[Font,
|
|
AbstractStyle],
|
|
background=None,
|
|
previous: Union['Word',
|
|
None] = None):
|
|
"""
|
|
Initialize a new Word.
|
|
|
|
Args:
|
|
text: The text content of the word
|
|
style: AbstractStyle object or Font object (for backward compatibility)
|
|
background: Optional background color override
|
|
previous: Reference to the previous word in sequence
|
|
"""
|
|
self._text = text
|
|
self._style = style
|
|
self._background = background
|
|
self._previous = previous
|
|
self._next = None
|
|
self.concrete = None
|
|
if previous:
|
|
previous.add_next(self)
|
|
|
|
@classmethod
|
|
def create_and_add_to(cls, text: str, container, style: Optional[Font] = None,
|
|
background=None) -> 'Word':
|
|
"""
|
|
Create a new Word and add it to a container, inheriting style and language
|
|
from the container if not explicitly provided.
|
|
|
|
This method provides a convenient way to create words that automatically
|
|
inherit styling from their container (Paragraph, FormattedSpan, etc.)
|
|
without copying string values - using object references instead.
|
|
|
|
Args:
|
|
text: The text content of the word
|
|
container: The container to add the word to (must have add_word method and style property)
|
|
style: Optional Font style override. If None, inherits from container
|
|
background: Optional background color override. If None, inherits from container
|
|
|
|
Returns:
|
|
The newly created Word object
|
|
|
|
Raises:
|
|
AttributeError: If the container doesn't have the required add_word method or style property
|
|
"""
|
|
# Inherit style from container if not provided
|
|
if style is None:
|
|
if hasattr(container, 'style'):
|
|
style = container.style
|
|
else:
|
|
raise AttributeError(
|
|
f"Container {
|
|
type(container).__name__} must have a 'style' property")
|
|
|
|
# Inherit background from container if not provided
|
|
if background is None and hasattr(container, 'background'):
|
|
background = container.background
|
|
|
|
# Determine the previous word for proper linking
|
|
previous = None
|
|
if hasattr(container, '_words') and container._words:
|
|
# Container has a _words list (like FormattedSpan)
|
|
previous = container._words[-1]
|
|
elif hasattr(container, 'words'):
|
|
# Container has a words() method (like Paragraph)
|
|
try:
|
|
# Get the last word from the iterator
|
|
for _, word in container.words():
|
|
previous = word
|
|
except (StopIteration, TypeError):
|
|
previous = None
|
|
|
|
# Create the new word
|
|
word = cls(text, style, background, previous)
|
|
|
|
# Link the previous word to this new one
|
|
if previous:
|
|
previous.add_next(word)
|
|
|
|
# Add the word to the container
|
|
if hasattr(container, 'add_word'):
|
|
# Check if add_word expects a Word object or text string
|
|
import inspect
|
|
sig = inspect.signature(container.add_word)
|
|
params = list(sig.parameters.keys())
|
|
|
|
if len(params) > 0:
|
|
# Peek at the parameter name to guess the expected type
|
|
param_name = params[0]
|
|
if param_name in ['word', 'word_obj', 'word_object']:
|
|
# Expects a Word object
|
|
container.add_word(word)
|
|
else:
|
|
# Might expect text string (like FormattedSpan.add_word)
|
|
# In this case, we can't use the container's add_word as it would create
|
|
# a duplicate Word. We need to add directly to the container's word
|
|
# list.
|
|
if hasattr(container, '_words'):
|
|
container._words.append(word)
|
|
else:
|
|
# Fallback: try calling with the Word object anyway
|
|
container.add_word(word)
|
|
else:
|
|
# No parameters, shouldn't happen with add_word methods
|
|
container.add_word(word)
|
|
else:
|
|
raise AttributeError(
|
|
f"Container {
|
|
type(container).__name__} must have an 'add_word' method")
|
|
|
|
return word
|
|
|
|
def add_concete(self, text: Union[Any, Tuple[Any, Any]]):
|
|
self.concrete = text
|
|
|
|
@property
|
|
def text(self) -> str:
|
|
"""Get the text content of the word"""
|
|
return self._text
|
|
|
|
@property
|
|
def style(self) -> Font:
|
|
"""Get the font style of the word"""
|
|
return self._style
|
|
|
|
@property
|
|
def background(self):
|
|
"""Get the background color of the word"""
|
|
return self._background
|
|
|
|
@property
|
|
def previous(self) -> Union['Word', None]:
|
|
"""Get the previous word in sequence"""
|
|
return self._previous
|
|
|
|
@property
|
|
def next(self) -> Union['Word', None]:
|
|
"""Get the next word in sequence"""
|
|
return self._next
|
|
|
|
def add_next(self, next_word: 'Word'):
|
|
"""Set the next word in sequence"""
|
|
self._next = next_word
|
|
|
|
def possible_hyphenation(self, language: str = None) -> bool:
|
|
"""
|
|
Hyphenate the word and store the parts.
|
|
|
|
Args:
|
|
language: Language code for hyphenation. If None, uses the style's language.
|
|
|
|
Returns:
|
|
bool: True if the word was hyphenated, False otherwise.
|
|
"""
|
|
|
|
dic = pyphen.Pyphen(lang=self._style.language)
|
|
return list(dic.iterate(self._text))
|
|
|
|
|
|
...
|
|
|
|
|
|
class FormattedSpan:
|
|
"""
|
|
A run of words with consistent formatting.
|
|
This represents a sequence of words that share the same style attributes.
|
|
"""
|
|
|
|
def __init__(self, style: Font, background=None):
|
|
"""
|
|
Initialize a new formatted span.
|
|
|
|
Args:
|
|
style: Font style information for all words in this span
|
|
background: Optional background color override
|
|
"""
|
|
self._style = style
|
|
self._background = background if background else style.background
|
|
self._words: List[Word] = []
|
|
|
|
@classmethod
|
|
def create_and_add_to(
|
|
cls,
|
|
container,
|
|
style: Optional[Font] = None,
|
|
background=None) -> 'FormattedSpan':
|
|
"""
|
|
Create a new FormattedSpan and add it to a container, inheriting style from
|
|
the container if not explicitly provided.
|
|
|
|
Args:
|
|
container: The container to add the span to (must have add_span method and style property)
|
|
style: Optional Font style override. If None, inherits from container
|
|
background: Optional background color override
|
|
|
|
Returns:
|
|
The newly created FormattedSpan object
|
|
|
|
Raises:
|
|
AttributeError: If the container doesn't have the required add_span method or style property
|
|
"""
|
|
# Inherit style from container if not provided
|
|
if style is None:
|
|
if hasattr(container, 'style'):
|
|
style = container.style
|
|
else:
|
|
raise AttributeError(
|
|
f"Container {
|
|
type(container).__name__} must have a 'style' property")
|
|
|
|
# Inherit background from container if not provided
|
|
if background is None and hasattr(container, 'background'):
|
|
background = container.background
|
|
|
|
# Create the new span
|
|
span = cls(style, background)
|
|
|
|
# Add the span to the container
|
|
if hasattr(container, 'add_span'):
|
|
container.add_span(span)
|
|
else:
|
|
raise AttributeError(
|
|
f"Container {
|
|
type(container).__name__} must have an 'add_span' method")
|
|
|
|
return span
|
|
|
|
@property
|
|
def style(self) -> Font:
|
|
"""Get the font style of this span"""
|
|
return self._style
|
|
|
|
@property
|
|
def background(self):
|
|
"""Get the background color of this span"""
|
|
return self._background
|
|
|
|
@property
|
|
def words(self) -> List[Word]:
|
|
"""Get the list of words in this span"""
|
|
return self._words
|
|
|
|
def add_word(self, text: str) -> Word:
|
|
"""
|
|
Create and add a new word to this span.
|
|
|
|
Args:
|
|
text: The text content of the word
|
|
|
|
Returns:
|
|
The newly created Word object
|
|
"""
|
|
# Get the previous word if any
|
|
previous = self._words[-1] if self._words else None
|
|
|
|
# Create the new word
|
|
word = Word(text, self._style, self._background, previous)
|
|
|
|
# Link the previous word to this new one
|
|
if previous:
|
|
previous.add_next(word)
|
|
|
|
# Add the word to our list
|
|
self._words.append(word)
|
|
|
|
return word
|
|
|
|
|
|
class LinkedWord(Word):
|
|
"""
|
|
A Word that is also a Link - combines text content with hyperlink functionality.
|
|
|
|
When a word is part of a hyperlink, it becomes clickable and can trigger
|
|
navigation or callbacks. Multiple words can share the same link destination.
|
|
"""
|
|
|
|
def __init__(self, text: str, style: Union[Font, 'AbstractStyle'],
|
|
location: str, link_type: Optional['LinkType'] = None,
|
|
callback: Optional[Callable] = None,
|
|
background=None, previous: Optional[Word] = None,
|
|
params: Optional[Dict[str, Any]] = None,
|
|
title: Optional[str] = None):
|
|
"""
|
|
Initialize a linked word.
|
|
|
|
Args:
|
|
text: The text content of the word
|
|
style: The font style
|
|
location: The link target (URL, bookmark, etc.)
|
|
link_type: Type of link (INTERNAL, EXTERNAL, etc.)
|
|
callback: Optional callback for link activation
|
|
background: Optional background color
|
|
previous: Previous word in sequence
|
|
params: Parameters for the link
|
|
title: Tooltip/title for the link
|
|
"""
|
|
# Initialize Word first
|
|
super().__init__(text, style, background, previous)
|
|
|
|
# Store link properties
|
|
self._location = location
|
|
self._link_type = link_type or LinkType.EXTERNAL
|
|
self._callback = callback
|
|
self._params = params or {}
|
|
self._title = title
|
|
|
|
@property
|
|
def location(self) -> str:
|
|
"""Get the link target location"""
|
|
return self._location
|
|
|
|
@property
|
|
def link_type(self):
|
|
"""Get the type of link"""
|
|
return self._link_type
|
|
|
|
@property
|
|
def link_callback(self) -> Optional[Callable]:
|
|
"""Get the link callback (distinct from word callback)"""
|
|
return self._callback
|
|
|
|
@property
|
|
def params(self) -> Dict[str, Any]:
|
|
"""Get the link parameters"""
|
|
return self._params
|
|
|
|
@property
|
|
def link_title(self) -> Optional[str]:
|
|
"""Get the link title/tooltip"""
|
|
return self._title
|
|
|
|
def execute_link(self, context: Optional[Dict[str, Any]] = None) -> Any:
|
|
"""
|
|
Execute the link action.
|
|
|
|
Args:
|
|
context: Optional context dict (e.g., {'text': word.text})
|
|
|
|
Returns:
|
|
The result of the link execution
|
|
"""
|
|
# Add word text to context
|
|
full_context = {**self._params, 'text': self._text}
|
|
if context:
|
|
full_context.update(context)
|
|
|
|
if self._link_type in (LinkType.API, LinkType.FUNCTION) and self._callback:
|
|
return self._callback(self._location, **full_context)
|
|
else:
|
|
# For INTERNAL and EXTERNAL links, return the location
|
|
return self._location
|
|
|
|
|
|
class LineBreak(Hierarchical):
|
|
"""
|
|
A line break element that forces a new line within text content.
|
|
While this is an inline element that can occur within paragraphs,
|
|
it has block-like properties for consistency with the abstract model.
|
|
|
|
Uses Hierarchical mixin for parent-child relationship management.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize a line break element."""
|
|
super().__init__()
|
|
# Import here to avoid circular imports
|
|
from .block import BlockType
|
|
self._block_type = BlockType.LINE_BREAK
|
|
|
|
@property
|
|
def block_type(self):
|
|
"""Get the block type for this line break"""
|
|
return self._block_type
|
|
|
|
@classmethod
|
|
def create_and_add_to(cls, container) -> 'LineBreak':
|
|
"""
|
|
Create a new LineBreak and add it to a container.
|
|
|
|
Args:
|
|
container: The container to add the line break to
|
|
|
|
Returns:
|
|
The newly created LineBreak object
|
|
"""
|
|
# Create the new line break
|
|
line_break = cls()
|
|
|
|
# Add the line break to the container if it has an appropriate method
|
|
if hasattr(container, 'add_line_break'):
|
|
container.add_line_break(line_break)
|
|
elif hasattr(container, 'add_element'):
|
|
container.add_element(line_break)
|
|
elif hasattr(container, 'add_word'):
|
|
# Some containers might treat line breaks like words
|
|
container.add_word(line_break)
|
|
else:
|
|
# Set parent relationship manually
|
|
line_break.parent = container
|
|
|
|
return line_break
|