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