Coverage for pyWebLayout/abstract/inline.py: 99%
157 statements
« prev ^ index » next coverage.py v7.11.2, created at 2025-11-12 12:02 +0000
« prev ^ index » next coverage.py v7.11.2, created at 2025-11-12 12:02 +0000
1from __future__ import annotations
2from pyWebLayout.core import Hierarchical
3from pyWebLayout.style import Font
4from pyWebLayout.style.abstract_style import AbstractStyle
5from typing import Tuple, Union, List, Optional, Dict, Any, Callable
6import pyphen
8# Import LinkType for type hints (imported at module level to avoid F821 linting error)
9from pyWebLayout.abstract.functional import LinkType
12class Word:
13 """
14 An abstract representation of a word in a document. Words can be split across
15 lines or pages during rendering. This class manages the logical representation
16 of a word without any rendering specifics.
18 Now uses AbstractStyle objects for memory efficiency and proper style management.
19 """
21 def __init__(self,
22 text: str,
23 style: Union[Font,
24 AbstractStyle],
25 background=None,
26 previous: Union['Word',
27 None] = None):
28 """
29 Initialize a new Word.
31 Args:
32 text: The text content of the word
33 style: AbstractStyle object or Font object (for backward compatibility)
34 background: Optional background color override
35 previous: Reference to the previous word in sequence
36 """
37 self._text = text
38 self._style = style
39 self._background = background
40 self._previous = previous
41 self._next = None
42 self.concrete = None
43 if previous:
44 previous.add_next(self)
46 @classmethod
47 def create_and_add_to(cls, text: str, container, style: Optional[Font] = None,
48 background=None) -> 'Word':
49 """
50 Create a new Word and add it to a container, inheriting style and language
51 from the container if not explicitly provided.
53 This method provides a convenient way to create words that automatically
54 inherit styling from their container (Paragraph, FormattedSpan, etc.)
55 without copying string values - using object references instead.
57 Args:
58 text: The text content of the word
59 container: The container to add the word to (must have add_word method and style property)
60 style: Optional Font style override. If None, inherits from container
61 background: Optional background color override. If None, inherits from container
63 Returns:
64 The newly created Word object
66 Raises:
67 AttributeError: If the container doesn't have the required add_word method or style property
68 """
69 # Inherit style from container if not provided
70 if style is None:
71 if hasattr(container, 'style'):
72 style = container.style
73 else:
74 raise AttributeError(
75 f"Container {type(container).__name__} must have a 'style' property")
77 # Inherit background from container if not provided
78 if background is None and hasattr(container, 'background'):
79 background = container.background
81 # Determine the previous word for proper linking
82 previous = None
83 if hasattr(container, '_words') and container._words:
84 # Container has a _words list (like FormattedSpan)
85 previous = container._words[-1]
86 elif hasattr(container, 'words'):
87 # Container has a words() method (like Paragraph)
88 try:
89 # Get the last word from the iterator
90 for _, word in container.words():
91 previous = word
92 except (StopIteration, TypeError):
93 previous = None
95 # Create the new word
96 word = cls(text, style, background, previous)
98 # Link the previous word to this new one
99 if previous:
100 previous.add_next(word)
102 # Add the word to the container
103 if hasattr(container, 'add_word'):
104 # Check if add_word expects a Word object or text string
105 import inspect
106 sig = inspect.signature(container.add_word)
107 params = list(sig.parameters.keys())
109 if len(params) > 0:
110 # Peek at the parameter name to guess the expected type
111 param_name = params[0]
112 if param_name in ['word', 'word_obj', 'word_object']:
113 # Expects a Word object
114 container.add_word(word)
115 else:
116 # Might expect text string (like FormattedSpan.add_word)
117 # In this case, we can't use the container's add_word as it would create
118 # a duplicate Word. We need to add directly to the container's word
119 # list.
120 if hasattr(container, '_words'):
121 container._words.append(word)
122 else:
123 # Fallback: try calling with the Word object anyway
124 container.add_word(word)
125 else:
126 # No parameters, shouldn't happen with add_word methods
127 container.add_word(word)
128 else:
129 raise AttributeError(
130 f"Container {type(container).__name__} must have an 'add_word' method")
132 return word
134 def add_concete(self, text: Union[Any, Tuple[Any, Any]]):
135 self.concrete = text
137 @property
138 def text(self) -> str:
139 """Get the text content of the word"""
140 return self._text
142 @property
143 def style(self) -> Font:
144 """Get the font style of the word"""
145 return self._style
147 @property
148 def background(self):
149 """Get the background color of the word"""
150 return self._background
152 @property
153 def previous(self) -> Union['Word', None]:
154 """Get the previous word in sequence"""
155 return self._previous
157 @property
158 def next(self) -> Union['Word', None]:
159 """Get the next word in sequence"""
160 return self._next
162 def add_next(self, next_word: 'Word'):
163 """Set the next word in sequence"""
164 self._next = next_word
166 def possible_hyphenation(self, language: str = None) -> bool:
167 """
168 Hyphenate the word and store the parts.
170 Args:
171 language: Language code for hyphenation. If None, uses the style's language.
173 Returns:
174 bool: True if the word was hyphenated, False otherwise.
175 """
177 dic = pyphen.Pyphen(lang=self._style.language)
178 return list(dic.iterate(self._text))
181...
184class FormattedSpan:
185 """
186 A run of words with consistent formatting.
187 This represents a sequence of words that share the same style attributes.
188 """
190 def __init__(self, style: Font, background=None):
191 """
192 Initialize a new formatted span.
194 Args:
195 style: Font style information for all words in this span
196 background: Optional background color override
197 """
198 self._style = style
199 self._background = background if background else style.background
200 self._words: List[Word] = []
202 @classmethod
203 def create_and_add_to(
204 cls,
205 container,
206 style: Optional[Font] = None,
207 background=None) -> 'FormattedSpan':
208 """
209 Create a new FormattedSpan and add it to a container, inheriting style from
210 the container if not explicitly provided.
212 Args:
213 container: The container to add the span to (must have add_span method and style property)
214 style: Optional Font style override. If None, inherits from container
215 background: Optional background color override
217 Returns:
218 The newly created FormattedSpan object
220 Raises:
221 AttributeError: If the container doesn't have the required add_span method or style property
222 """
223 # Inherit style from container if not provided
224 if style is None:
225 if hasattr(container, 'style'):
226 style = container.style
227 else:
228 raise AttributeError(
229 f"Container {type(container).__name__} must have a 'style' property")
231 # Inherit background from container if not provided
232 if background is None and hasattr(container, 'background'):
233 background = container.background
235 # Create the new span
236 span = cls(style, background)
238 # Add the span to the container
239 if hasattr(container, 'add_span'):
240 container.add_span(span)
241 else:
242 raise AttributeError(
243 f"Container {type(container).__name__} must have an 'add_span' method")
245 return span
247 @property
248 def style(self) -> Font:
249 """Get the font style of this span"""
250 return self._style
252 @property
253 def background(self):
254 """Get the background color of this span"""
255 return self._background
257 @property
258 def words(self) -> List[Word]:
259 """Get the list of words in this span"""
260 return self._words
262 def add_word(self, text: str) -> Word:
263 """
264 Create and add a new word to this span.
266 Args:
267 text: The text content of the word
269 Returns:
270 The newly created Word object
271 """
272 # Get the previous word if any
273 previous = self._words[-1] if self._words else None
275 # Create the new word
276 word = Word(text, self._style, self._background, previous)
278 # Link the previous word to this new one
279 if previous:
280 previous.add_next(word)
282 # Add the word to our list
283 self._words.append(word)
285 return word
288class LinkedWord(Word):
289 """
290 A Word that is also a Link - combines text content with hyperlink functionality.
292 When a word is part of a hyperlink, it becomes clickable and can trigger
293 navigation or callbacks. Multiple words can share the same link destination.
294 """
296 def __init__(self, text: str, style: Union[Font, 'AbstractStyle'],
297 location: str, link_type: Optional['LinkType'] = None,
298 callback: Optional[Callable] = None,
299 background=None, previous: Optional[Word] = None,
300 params: Optional[Dict[str, Any]] = None,
301 title: Optional[str] = None):
302 """
303 Initialize a linked word.
305 Args:
306 text: The text content of the word
307 style: The font style
308 location: The link target (URL, bookmark, etc.)
309 link_type: Type of link (INTERNAL, EXTERNAL, etc.)
310 callback: Optional callback for link activation
311 background: Optional background color
312 previous: Previous word in sequence
313 params: Parameters for the link
314 title: Tooltip/title for the link
315 """
316 # Initialize Word first
317 super().__init__(text, style, background, previous)
319 # Store link properties
320 self._location = location
321 self._link_type = link_type or LinkType.EXTERNAL
322 self._callback = callback
323 self._params = params or {}
324 self._title = title
326 @property
327 def location(self) -> str:
328 """Get the link target location"""
329 return self._location
331 @property
332 def link_type(self):
333 """Get the type of link"""
334 return self._link_type
336 @property
337 def link_callback(self) -> Optional[Callable]:
338 """Get the link callback (distinct from word callback)"""
339 return self._callback
341 @property
342 def params(self) -> Dict[str, Any]:
343 """Get the link parameters"""
344 return self._params
346 @property
347 def link_title(self) -> Optional[str]:
348 """Get the link title/tooltip"""
349 return self._title
351 def execute_link(self, context: Optional[Dict[str, Any]] = None) -> Any:
352 """
353 Execute the link action.
355 Args:
356 context: Optional context dict (e.g., {'text': word.text})
358 Returns:
359 The result of the link execution
360 """
361 # Add word text to context
362 full_context = {**self._params, 'text': self._text}
363 if context: 363 ↛ 364line 363 didn't jump to line 364 because the condition on line 363 was never true
364 full_context.update(context)
366 if self._link_type in (LinkType.API, LinkType.FUNCTION) and self._callback:
367 return self._callback(self._location, **full_context)
368 else:
369 # For INTERNAL and EXTERNAL links, return the location
370 return self._location
373class LineBreak(Hierarchical):
374 """
375 A line break element that forces a new line within text content.
376 While this is an inline element that can occur within paragraphs,
377 it has block-like properties for consistency with the abstract model.
379 Uses Hierarchical mixin for parent-child relationship management.
380 """
382 def __init__(self):
383 """Initialize a line break element."""
384 super().__init__()
385 # Import here to avoid circular imports
386 from .block import BlockType
387 self._block_type = BlockType.LINE_BREAK
389 @property
390 def block_type(self):
391 """Get the block type for this line break"""
392 return self._block_type
394 @classmethod
395 def create_and_add_to(cls, container) -> 'LineBreak':
396 """
397 Create a new LineBreak and add it to a container.
399 Args:
400 container: The container to add the line break to
402 Returns:
403 The newly created LineBreak object
404 """
405 # Create the new line break
406 line_break = cls()
408 # Add the line break to the container if it has an appropriate method
409 if hasattr(container, 'add_line_break'):
410 container.add_line_break(line_break)
411 elif hasattr(container, 'add_element'):
412 container.add_element(line_break)
413 elif hasattr(container, 'add_word'):
414 # Some containers might treat line breaks like words
415 container.add_word(line_break)
416 else:
417 # Set parent relationship manually
418 line_break.parent = container
420 return line_break