html_compose.base_element
1from abc import ABCMeta 2from typing import Any, Callable, Generator, Iterable, Mapping, TypeVar, cast 3 4from . import escape_text, unsafe_text, util_funcs 5from .attributes import BaseAttribute, GlobalAttrs 6from .base_types import ElementBase, Node, Resolvable, _HasHtml 7 8SPECIAL_ATTRS = {"class": GlobalAttrs.class_, "style": GlobalAttrs.style} 9 10 11T = TypeVar("T", bound="BaseElement") 12 13 14class ElementMeta(ABCMeta): 15 """ 16 The metaclass for all HTML elements 17 """ 18 19 # We aggressively hack the type checker here 20 def __getitem__(cls: type[T], key: Node) -> T: # type: ignore # pyright: ignore[reportGeneralTypeIssues] 21 """ 22 This implements a shortcut to the constructor for a given element. 23 24 Example: 25 If the user passes `h1["Demo"]` the user likely expects h1()["Demo"] 26 """ 27 inst = cls() # type: ignore # pyright: ignore[reportCallIssue] 28 inst.append(key) 29 return inst 30 31 32class BaseElement(ElementBase, metaclass=ElementMeta): 33 """ 34 Base HTML element 35 36 All elements derive from this class 37 """ 38 39 __slots__ = ("tag", "attrs", "_children", "is_void_element") 40 41 def __getitem__(self, key): 42 """ 43 Implements [] syntax which automatically appends to children list. 44 45 Example: 46 47 div()[ 48 "text", 49 p()["text"], 50 ul()[ 51 li["a"], 52 li["b"] 53 ] 54 ] 55 """ 56 # todo: consider raising based on type 57 # todo: consider raising if chained: 58 # div()[1,2][3] 59 self.append(key) 60 61 return self 62 63 def __init__( 64 self, 65 tag: str, 66 void_element: bool = False, 67 attrs: Iterable[BaseAttribute] 68 | Mapping[str, Resolvable] 69 | Iterable[ 70 BaseAttribute | Iterable[BaseAttribute] | Mapping[str, Resolvable] 71 ] 72 | None = None, 73 children: list | None = None, 74 ) -> None: 75 """ 76 Initialize an HTML element 77 78 Args: 79 tag (str): The tag of the element. 80 void_element (bool): Indicates if the element is a void element. Defaults to False. 81 attrs: A list of attributes for the element. 82 It can also be a dictionary of key,value strings. 83 Defaults to None. 84 children: A list of child elements. Defaults to None. 85 """ 86 self.tag: str = tag 87 self.attrs: dict[str, str] = self._resolve_attrs(attrs) 88 89 self._children: list[Node] = children if children else [] 90 self.is_void_element: bool = void_element 91 92 def __eq__(self, other: Any): 93 """Compare rendered HTML instead of class data""" 94 if isinstance(other, self.__class__): 95 return self.render() == other.render() 96 97 if isinstance(other, str): 98 return self.render() == other 99 100 return False 101 102 def _process_attr( 103 self, attr_name: str, attr_data: str | Resolvable | BaseAttribute | None 104 ): 105 """ 106 Add an attribute for the element to the internal _attrs dict 107 We technically allow stacking for supported attributes. 108 This allows us to support (combine) attributes like "class" and "style". 109 110 Args: 111 attr_name (str): The name of the attribute. 112 attr_data (str | Resolvable): The data for the attribute. 113 """ 114 if attr_data is None or attr_data is False: 115 return # noop 116 117 if isinstance(attr_data, BaseAttribute): 118 attr = attr_data 119 else: 120 attr_class = SPECIAL_ATTRS.get(attr_name, None) 121 if attr_class: 122 attr = attr_class(attr_data) 123 else: 124 attr = BaseAttribute(attr_name, attr_data) 125 126 result = attr.evaluate() 127 if result is not None: 128 _, resolved_value = result 129 if attr_name in self.attrs: 130 if attr_name == "class": 131 self.attrs[attr_name] = ( 132 f"{self.attrs[attr_name]} {resolved_value}" 133 ) 134 elif attr_name == "style": 135 self.attrs[attr_name] = ( 136 f"{self.attrs[attr_name]}; {resolved_value}" 137 ) 138 else: 139 raise ValueError( 140 f"Attribute {attr_name} was passed twice. " 141 "We don't know how to merge it." 142 ) 143 else: 144 self.attrs[attr_name] = resolved_value 145 146 def _resolve_attrs( 147 self, 148 attrs: Iterable[BaseAttribute] 149 | Mapping[str, Resolvable] 150 | Iterable[ 151 BaseAttribute | Iterable[BaseAttribute] | Mapping[str, Resolvable] 152 ] 153 | None, 154 ) -> dict[str, str]: 155 """ 156 Resolve attributes into key/value pairs 157 """ 158 if not attrs: 159 return {} 160 161 attr_dict: dict[str, str] = {} 162 # These are sent to us in format: 163 # key, value (unescaped) 164 if isinstance(attrs, (list, tuple)): 165 for item in attrs: 166 if isinstance(item, BaseAttribute): 167 result = item.evaluate() 168 if not result: 169 continue 170 key, value = result 171 attr_dict[key] = value 172 elif isinstance(item, tuple) and len(item) == 2: 173 # no runtime checking here, but hint the type checker 174 item = cast(tuple[str, Resolvable], item) 175 176 attr = BaseAttribute(name=item[0], data=item[1]).evaluate() 177 if not attr: 178 continue 179 180 a_name, a_value = attr 181 attr_dict[a_name] = a_value 182 elif isinstance(item, Mapping): 183 # no runtime checking here, but hint the type checker 184 item = cast(Mapping[str, Resolvable], item) 185 186 for key, value in item.items(): # type: ignore[assignment] 187 attr = BaseAttribute(key, value).evaluate() 188 if not attr: 189 continue 190 a_name, a_value = attr 191 attr_dict[a_name] = a_value 192 else: 193 raise ValueError( 194 f"Unknown type for attr value: {type(item)}." 195 ) 196 197 elif isinstance(attrs, dict): 198 # hint the type checker 199 attrs = cast(Mapping[str, Resolvable], attrs) 200 201 for key, value in attrs.items(): # type: ignore[assignment] 202 attr = BaseAttribute(key, value).evaluate() 203 if attr: 204 a_name, a_value = attr 205 attr_dict[a_name] = a_value 206 207 else: 208 raise ValueError(f"Unknown: {type(attrs)}") 209 210 return attr_dict 211 212 def _call_callable( 213 self, func: Callable, parent: ElementBase | None 214 ) -> Node: 215 """ 216 Executor for callable elements 217 218 These elements may accept 0-2 positional args: 219 0: None 220 1: self (The function may consider it "parent") 221 2: the parents parent 222 223 """ 224 param_count = util_funcs.get_param_count(func) 225 226 assert param_count in range(0, 3), ( 227 "Element resolution expects 0 - 2 parameter callables" 228 f", got {param_count} params" 229 ) 230 231 if param_count == 0: 232 result = func() 233 elif param_count == 1: 234 result = func(self) 235 elif param_count == 2: 236 result = func(self, parent) 237 else: 238 raise ValueError( 239 "Lambda received has too many parameters to process" 240 ) 241 # assume the result is a Node, including None 242 return cast(Node, result) 243 244 def _resolve_child( 245 self, child: Node, call_callables: bool, parent: ElementBase | None 246 ) -> Generator[str, None, None]: 247 """ 248 Child resolver for elements 249 250 Returns raw HTML string for child 251 252 If call_callables is false, callables are yielded. 253 """ 254 255 if child is None: 256 # null child, possibly from the callable. 257 # Magic: We ignore null children for things like 258 # div[ 259 # button if needs_button else None 260 # ] 261 yield from () 262 263 elif isinstance(child, ElementBase): 264 # Recursively resolve the element tree 265 yield from child.resolve(self) 266 267 elif isinstance(child, ElementMeta) and not hasattr(child, "__self__"): 268 # This is an uninstantiated class-based element like elements.br 269 inst: BaseElement = child() 270 yield from inst.resolve() 271 272 elif hasattr(child, "__html__"): 273 # Duck typing is faster than isinstance checks on runtime Protocols 274 yield unsafe_text(cast(_HasHtml, child).__html__()) 275 276 elif isinstance(child, str): 277 # Magic: If the string is already escaped, this never has to fire. 278 yield escape_text(child) 279 280 elif isinstance(child, int): 281 # escape_text will str() 282 yield escape_text(child) 283 284 elif isinstance(child, float): 285 # Magic: Convert float to string with fixed ndigits 286 # This avoids weird output like 6.33333333333... 287 precision = self.__class__.FLOAT_PRECISION 288 rounded = round(child, precision) 289 if precision == 0: 290 # Cut off decimal point in this case. 291 rounded = int(rounded) 292 yield escape_text(rounded) 293 294 elif isinstance(child, bool): 295 # Magic: Convert to 'typical' true/false 296 # Most people using this would be better using None 297 # which specifically means "no render" 298 # But some weirdos may be trying to render true/false literally 299 yield unsafe_text("true" if child else "false") 300 301 elif util_funcs.is_iterable_but_not_str(child): 302 for el in util_funcs.flatten_iterable(child): # type: ignore[arg-type] 303 yield from self._resolve_child(el, call_callables, parent) 304 305 elif callable(child): 306 if not call_callables: 307 # In deferred resolve state, 308 # callables are yielded instead of resolved 309 yield child # type: ignore[misc] 310 else: 311 result = child 312 while callable(result): 313 result = self._call_callable(result, parent) # type: ignore[assignment] 314 315 yield from self._resolve_child(result, call_callables, parent) 316 else: 317 raise ValueError(f"Unknown child type: {type(child)}") 318 319 def _resolve_tree( 320 self, parent: ElementBase | None = None 321 ) -> Generator[str | Callable[..., Node], None, None]: 322 """ 323 Walk html element tree and yield all resolved children 324 325 Callables are yielded instead of resolved 326 327 Return: 328 escaped (trusted) HTML strings 329 """ 330 331 for child in self._children: 332 yield from self._resolve_child( 333 child, call_callables=False, parent=parent 334 ) 335 336 def append(self, *child_or_childs: Node): 337 """ 338 This method appends one or more elements to the list of children 339 under this element. 340 341 In order to facilitate fluid tree construction, it accepts: 342 * A single child element, like a text string or another element 343 * A callable that returns a valid child when called 344 * A list, tuple, or iterable of child elements or callables 345 * n positional arguments which may be a mix of child elements, 346 callables, or iterables 347 348 349 This method is the backbone for the `[]` syntax. 350 351 Parameters 352 ---------- 353 `child_or_childs`: 354 Any acceptable child including iterables and callables. 355 """ 356 if self.is_void_element: 357 raise ValueError(f"Void element {self.tag} cannot have children") 358 359 args = child_or_childs 360 # Special case: We may have been passed a literal tuple 361 # If it has one child that itself is a tuple, unbox it. 362 if ( 363 isinstance(args, tuple) 364 and len(args) == 1 365 and isinstance(args[0], tuple) 366 ): 367 args = args[0] 368 369 # Unbox any literal tuple, lists 370 if isinstance(args, tuple) or isinstance(args, list): 371 for k in args: 372 self._children.append(k) 373 else: 374 # Let the child resolver step handle it 375 # Applies to iterables, callables, literal elements 376 self._children.append(args) 377 378 def deferred_resolve( 379 self, parent: ElementBase | None = None 380 ) -> Generator[Node, None, None]: 381 """ 382 Resolve all attributes and children of the HTML element, except for callable children. 383 384 This method performs the following steps: 385 1. Resolves all attributes of the element. 386 2. Resolves all non-callable children. 387 3. Applies context hooks if applicable. 388 4. Generates the HTML string representation of the element. 389 390 Returns: 391 Generator[str, None, None]: A generator that yields strings representing 392 parts of the HTML element. These parts include: 393 - The opening tag with attributes 394 - The content (children) of the element 395 - The closing tag 396 397 Note: 398 - For void elements, only the self-closing tag is yielded. 399 - Callable children are not resolved in this method. 400 """ 401 402 # attrs is a defaultdict of strings. 403 # The key is the attr name, the value is the attr value unescaped. 404 attrs = self.attrs 405 406 children = None 407 408 if not self.is_void_element: 409 children = (child for child in self._resolve_tree(parent)) 410 411 # join_attrs has a configurable lru_cache 412 join_attrs = self.get_attr_join() 413 414 # Generate the key="value" pairs for the attributes 415 # The value escape step lives here because we trust no 416 # previous step in the pipeline. 417 # Magic: Security: Escape all attr values 418 attr_string = " ".join( 419 (join_attrs(k, escape_text(v)) for k, v in attrs.items()) 420 ) 421 422 if self.is_void_element: 423 if attr_string: 424 yield f"<{self.tag} {attr_string}/>" 425 else: 426 yield f"<{self.tag}/>" 427 else: 428 if attr_string: 429 yield f"<{self.tag} {attr_string}>" 430 else: 431 yield f"<{self.tag}>" 432 if children is not None: 433 yield from children 434 yield f"</{self.tag}>" 435 436 def resolve( 437 self, parent: ElementBase | None = None 438 ) -> Generator[str, None, None]: 439 """ 440 Generate the flat HTML [string] iterator for the HTML element 441 """ 442 resolver = self.deferred_resolve(parent) 443 for element in resolver: 444 if callable(element): 445 # Feature: nested calling similar to a functional programming style 446 yield from self._resolve_child( 447 element, call_callables=True, parent=parent 448 ) 449 else: 450 yield cast(str, element) 451 452 def render(self, parent: ElementBase | None = None) -> str: 453 """ 454 Render the HTML element 455 """ 456 return "".join(self.resolve(parent)) 457 458 def __str__(self) -> str: 459 return self.__html__() 460 461 def __repr__(self) -> str: 462 children = [ 463 child for child in util_funcs.flatten_iterable(self._children) 464 ] 465 children_info = ", ".join( 466 repr(child) if not callable(child) else "<callable>" 467 for child in children 468 ) 469 astring = "" 470 if self.attrs: 471 astring = f"{self.attrs}" 472 cstring = "" 473 if children: 474 cstring = f"[{children_info}]" 475 return f"{self.__class__.__name__}({astring}){cstring}" 476 477 def __html__(self) -> str: 478 """ 479 Render the HTML element 480 """ 481 return self.render()
15class ElementMeta(ABCMeta): 16 """ 17 The metaclass for all HTML elements 18 """ 19 20 # We aggressively hack the type checker here 21 def __getitem__(cls: type[T], key: Node) -> T: # type: ignore # pyright: ignore[reportGeneralTypeIssues] 22 """ 23 This implements a shortcut to the constructor for a given element. 24 25 Example: 26 If the user passes `h1["Demo"]` the user likely expects h1()["Demo"] 27 """ 28 inst = cls() # type: ignore # pyright: ignore[reportCallIssue] 29 inst.append(key) 30 return inst
The metaclass for all HTML elements
33class BaseElement(ElementBase, metaclass=ElementMeta): 34 """ 35 Base HTML element 36 37 All elements derive from this class 38 """ 39 40 __slots__ = ("tag", "attrs", "_children", "is_void_element") 41 42 def __getitem__(self, key): 43 """ 44 Implements [] syntax which automatically appends to children list. 45 46 Example: 47 48 div()[ 49 "text", 50 p()["text"], 51 ul()[ 52 li["a"], 53 li["b"] 54 ] 55 ] 56 """ 57 # todo: consider raising based on type 58 # todo: consider raising if chained: 59 # div()[1,2][3] 60 self.append(key) 61 62 return self 63 64 def __init__( 65 self, 66 tag: str, 67 void_element: bool = False, 68 attrs: Iterable[BaseAttribute] 69 | Mapping[str, Resolvable] 70 | Iterable[ 71 BaseAttribute | Iterable[BaseAttribute] | Mapping[str, Resolvable] 72 ] 73 | None = None, 74 children: list | None = None, 75 ) -> None: 76 """ 77 Initialize an HTML element 78 79 Args: 80 tag (str): The tag of the element. 81 void_element (bool): Indicates if the element is a void element. Defaults to False. 82 attrs: A list of attributes for the element. 83 It can also be a dictionary of key,value strings. 84 Defaults to None. 85 children: A list of child elements. Defaults to None. 86 """ 87 self.tag: str = tag 88 self.attrs: dict[str, str] = self._resolve_attrs(attrs) 89 90 self._children: list[Node] = children if children else [] 91 self.is_void_element: bool = void_element 92 93 def __eq__(self, other: Any): 94 """Compare rendered HTML instead of class data""" 95 if isinstance(other, self.__class__): 96 return self.render() == other.render() 97 98 if isinstance(other, str): 99 return self.render() == other 100 101 return False 102 103 def _process_attr( 104 self, attr_name: str, attr_data: str | Resolvable | BaseAttribute | None 105 ): 106 """ 107 Add an attribute for the element to the internal _attrs dict 108 We technically allow stacking for supported attributes. 109 This allows us to support (combine) attributes like "class" and "style". 110 111 Args: 112 attr_name (str): The name of the attribute. 113 attr_data (str | Resolvable): The data for the attribute. 114 """ 115 if attr_data is None or attr_data is False: 116 return # noop 117 118 if isinstance(attr_data, BaseAttribute): 119 attr = attr_data 120 else: 121 attr_class = SPECIAL_ATTRS.get(attr_name, None) 122 if attr_class: 123 attr = attr_class(attr_data) 124 else: 125 attr = BaseAttribute(attr_name, attr_data) 126 127 result = attr.evaluate() 128 if result is not None: 129 _, resolved_value = result 130 if attr_name in self.attrs: 131 if attr_name == "class": 132 self.attrs[attr_name] = ( 133 f"{self.attrs[attr_name]} {resolved_value}" 134 ) 135 elif attr_name == "style": 136 self.attrs[attr_name] = ( 137 f"{self.attrs[attr_name]}; {resolved_value}" 138 ) 139 else: 140 raise ValueError( 141 f"Attribute {attr_name} was passed twice. " 142 "We don't know how to merge it." 143 ) 144 else: 145 self.attrs[attr_name] = resolved_value 146 147 def _resolve_attrs( 148 self, 149 attrs: Iterable[BaseAttribute] 150 | Mapping[str, Resolvable] 151 | Iterable[ 152 BaseAttribute | Iterable[BaseAttribute] | Mapping[str, Resolvable] 153 ] 154 | None, 155 ) -> dict[str, str]: 156 """ 157 Resolve attributes into key/value pairs 158 """ 159 if not attrs: 160 return {} 161 162 attr_dict: dict[str, str] = {} 163 # These are sent to us in format: 164 # key, value (unescaped) 165 if isinstance(attrs, (list, tuple)): 166 for item in attrs: 167 if isinstance(item, BaseAttribute): 168 result = item.evaluate() 169 if not result: 170 continue 171 key, value = result 172 attr_dict[key] = value 173 elif isinstance(item, tuple) and len(item) == 2: 174 # no runtime checking here, but hint the type checker 175 item = cast(tuple[str, Resolvable], item) 176 177 attr = BaseAttribute(name=item[0], data=item[1]).evaluate() 178 if not attr: 179 continue 180 181 a_name, a_value = attr 182 attr_dict[a_name] = a_value 183 elif isinstance(item, Mapping): 184 # no runtime checking here, but hint the type checker 185 item = cast(Mapping[str, Resolvable], item) 186 187 for key, value in item.items(): # type: ignore[assignment] 188 attr = BaseAttribute(key, value).evaluate() 189 if not attr: 190 continue 191 a_name, a_value = attr 192 attr_dict[a_name] = a_value 193 else: 194 raise ValueError( 195 f"Unknown type for attr value: {type(item)}." 196 ) 197 198 elif isinstance(attrs, dict): 199 # hint the type checker 200 attrs = cast(Mapping[str, Resolvable], attrs) 201 202 for key, value in attrs.items(): # type: ignore[assignment] 203 attr = BaseAttribute(key, value).evaluate() 204 if attr: 205 a_name, a_value = attr 206 attr_dict[a_name] = a_value 207 208 else: 209 raise ValueError(f"Unknown: {type(attrs)}") 210 211 return attr_dict 212 213 def _call_callable( 214 self, func: Callable, parent: ElementBase | None 215 ) -> Node: 216 """ 217 Executor for callable elements 218 219 These elements may accept 0-2 positional args: 220 0: None 221 1: self (The function may consider it "parent") 222 2: the parents parent 223 224 """ 225 param_count = util_funcs.get_param_count(func) 226 227 assert param_count in range(0, 3), ( 228 "Element resolution expects 0 - 2 parameter callables" 229 f", got {param_count} params" 230 ) 231 232 if param_count == 0: 233 result = func() 234 elif param_count == 1: 235 result = func(self) 236 elif param_count == 2: 237 result = func(self, parent) 238 else: 239 raise ValueError( 240 "Lambda received has too many parameters to process" 241 ) 242 # assume the result is a Node, including None 243 return cast(Node, result) 244 245 def _resolve_child( 246 self, child: Node, call_callables: bool, parent: ElementBase | None 247 ) -> Generator[str, None, None]: 248 """ 249 Child resolver for elements 250 251 Returns raw HTML string for child 252 253 If call_callables is false, callables are yielded. 254 """ 255 256 if child is None: 257 # null child, possibly from the callable. 258 # Magic: We ignore null children for things like 259 # div[ 260 # button if needs_button else None 261 # ] 262 yield from () 263 264 elif isinstance(child, ElementBase): 265 # Recursively resolve the element tree 266 yield from child.resolve(self) 267 268 elif isinstance(child, ElementMeta) and not hasattr(child, "__self__"): 269 # This is an uninstantiated class-based element like elements.br 270 inst: BaseElement = child() 271 yield from inst.resolve() 272 273 elif hasattr(child, "__html__"): 274 # Duck typing is faster than isinstance checks on runtime Protocols 275 yield unsafe_text(cast(_HasHtml, child).__html__()) 276 277 elif isinstance(child, str): 278 # Magic: If the string is already escaped, this never has to fire. 279 yield escape_text(child) 280 281 elif isinstance(child, int): 282 # escape_text will str() 283 yield escape_text(child) 284 285 elif isinstance(child, float): 286 # Magic: Convert float to string with fixed ndigits 287 # This avoids weird output like 6.33333333333... 288 precision = self.__class__.FLOAT_PRECISION 289 rounded = round(child, precision) 290 if precision == 0: 291 # Cut off decimal point in this case. 292 rounded = int(rounded) 293 yield escape_text(rounded) 294 295 elif isinstance(child, bool): 296 # Magic: Convert to 'typical' true/false 297 # Most people using this would be better using None 298 # which specifically means "no render" 299 # But some weirdos may be trying to render true/false literally 300 yield unsafe_text("true" if child else "false") 301 302 elif util_funcs.is_iterable_but_not_str(child): 303 for el in util_funcs.flatten_iterable(child): # type: ignore[arg-type] 304 yield from self._resolve_child(el, call_callables, parent) 305 306 elif callable(child): 307 if not call_callables: 308 # In deferred resolve state, 309 # callables are yielded instead of resolved 310 yield child # type: ignore[misc] 311 else: 312 result = child 313 while callable(result): 314 result = self._call_callable(result, parent) # type: ignore[assignment] 315 316 yield from self._resolve_child(result, call_callables, parent) 317 else: 318 raise ValueError(f"Unknown child type: {type(child)}") 319 320 def _resolve_tree( 321 self, parent: ElementBase | None = None 322 ) -> Generator[str | Callable[..., Node], None, None]: 323 """ 324 Walk html element tree and yield all resolved children 325 326 Callables are yielded instead of resolved 327 328 Return: 329 escaped (trusted) HTML strings 330 """ 331 332 for child in self._children: 333 yield from self._resolve_child( 334 child, call_callables=False, parent=parent 335 ) 336 337 def append(self, *child_or_childs: Node): 338 """ 339 This method appends one or more elements to the list of children 340 under this element. 341 342 In order to facilitate fluid tree construction, it accepts: 343 * A single child element, like a text string or another element 344 * A callable that returns a valid child when called 345 * A list, tuple, or iterable of child elements or callables 346 * n positional arguments which may be a mix of child elements, 347 callables, or iterables 348 349 350 This method is the backbone for the `[]` syntax. 351 352 Parameters 353 ---------- 354 `child_or_childs`: 355 Any acceptable child including iterables and callables. 356 """ 357 if self.is_void_element: 358 raise ValueError(f"Void element {self.tag} cannot have children") 359 360 args = child_or_childs 361 # Special case: We may have been passed a literal tuple 362 # If it has one child that itself is a tuple, unbox it. 363 if ( 364 isinstance(args, tuple) 365 and len(args) == 1 366 and isinstance(args[0], tuple) 367 ): 368 args = args[0] 369 370 # Unbox any literal tuple, lists 371 if isinstance(args, tuple) or isinstance(args, list): 372 for k in args: 373 self._children.append(k) 374 else: 375 # Let the child resolver step handle it 376 # Applies to iterables, callables, literal elements 377 self._children.append(args) 378 379 def deferred_resolve( 380 self, parent: ElementBase | None = None 381 ) -> Generator[Node, None, None]: 382 """ 383 Resolve all attributes and children of the HTML element, except for callable children. 384 385 This method performs the following steps: 386 1. Resolves all attributes of the element. 387 2. Resolves all non-callable children. 388 3. Applies context hooks if applicable. 389 4. Generates the HTML string representation of the element. 390 391 Returns: 392 Generator[str, None, None]: A generator that yields strings representing 393 parts of the HTML element. These parts include: 394 - The opening tag with attributes 395 - The content (children) of the element 396 - The closing tag 397 398 Note: 399 - For void elements, only the self-closing tag is yielded. 400 - Callable children are not resolved in this method. 401 """ 402 403 # attrs is a defaultdict of strings. 404 # The key is the attr name, the value is the attr value unescaped. 405 attrs = self.attrs 406 407 children = None 408 409 if not self.is_void_element: 410 children = (child for child in self._resolve_tree(parent)) 411 412 # join_attrs has a configurable lru_cache 413 join_attrs = self.get_attr_join() 414 415 # Generate the key="value" pairs for the attributes 416 # The value escape step lives here because we trust no 417 # previous step in the pipeline. 418 # Magic: Security: Escape all attr values 419 attr_string = " ".join( 420 (join_attrs(k, escape_text(v)) for k, v in attrs.items()) 421 ) 422 423 if self.is_void_element: 424 if attr_string: 425 yield f"<{self.tag} {attr_string}/>" 426 else: 427 yield f"<{self.tag}/>" 428 else: 429 if attr_string: 430 yield f"<{self.tag} {attr_string}>" 431 else: 432 yield f"<{self.tag}>" 433 if children is not None: 434 yield from children 435 yield f"</{self.tag}>" 436 437 def resolve( 438 self, parent: ElementBase | None = None 439 ) -> Generator[str, None, None]: 440 """ 441 Generate the flat HTML [string] iterator for the HTML element 442 """ 443 resolver = self.deferred_resolve(parent) 444 for element in resolver: 445 if callable(element): 446 # Feature: nested calling similar to a functional programming style 447 yield from self._resolve_child( 448 element, call_callables=True, parent=parent 449 ) 450 else: 451 yield cast(str, element) 452 453 def render(self, parent: ElementBase | None = None) -> str: 454 """ 455 Render the HTML element 456 """ 457 return "".join(self.resolve(parent)) 458 459 def __str__(self) -> str: 460 return self.__html__() 461 462 def __repr__(self) -> str: 463 children = [ 464 child for child in util_funcs.flatten_iterable(self._children) 465 ] 466 children_info = ", ".join( 467 repr(child) if not callable(child) else "<callable>" 468 for child in children 469 ) 470 astring = "" 471 if self.attrs: 472 astring = f"{self.attrs}" 473 cstring = "" 474 if children: 475 cstring = f"[{children_info}]" 476 return f"{self.__class__.__name__}({astring}){cstring}" 477 478 def __html__(self) -> str: 479 """ 480 Render the HTML element 481 """ 482 return self.render()
Base HTML element
All elements derive from this class
64 def __init__( 65 self, 66 tag: str, 67 void_element: bool = False, 68 attrs: Iterable[BaseAttribute] 69 | Mapping[str, Resolvable] 70 | Iterable[ 71 BaseAttribute | Iterable[BaseAttribute] | Mapping[str, Resolvable] 72 ] 73 | None = None, 74 children: list | None = None, 75 ) -> None: 76 """ 77 Initialize an HTML element 78 79 Args: 80 tag (str): The tag of the element. 81 void_element (bool): Indicates if the element is a void element. Defaults to False. 82 attrs: A list of attributes for the element. 83 It can also be a dictionary of key,value strings. 84 Defaults to None. 85 children: A list of child elements. Defaults to None. 86 """ 87 self.tag: str = tag 88 self.attrs: dict[str, str] = self._resolve_attrs(attrs) 89 90 self._children: list[Node] = children if children else [] 91 self.is_void_element: bool = void_element
Initialize an HTML element
Args: tag (str): The tag of the element. void_element (bool): Indicates if the element is a void element. Defaults to False. attrs: A list of attributes for the element. It can also be a dictionary of key,value strings. Defaults to None. children: A list of child elements. Defaults to None.
337 def append(self, *child_or_childs: Node): 338 """ 339 This method appends one or more elements to the list of children 340 under this element. 341 342 In order to facilitate fluid tree construction, it accepts: 343 * A single child element, like a text string or another element 344 * A callable that returns a valid child when called 345 * A list, tuple, or iterable of child elements or callables 346 * n positional arguments which may be a mix of child elements, 347 callables, or iterables 348 349 350 This method is the backbone for the `[]` syntax. 351 352 Parameters 353 ---------- 354 `child_or_childs`: 355 Any acceptable child including iterables and callables. 356 """ 357 if self.is_void_element: 358 raise ValueError(f"Void element {self.tag} cannot have children") 359 360 args = child_or_childs 361 # Special case: We may have been passed a literal tuple 362 # If it has one child that itself is a tuple, unbox it. 363 if ( 364 isinstance(args, tuple) 365 and len(args) == 1 366 and isinstance(args[0], tuple) 367 ): 368 args = args[0] 369 370 # Unbox any literal tuple, lists 371 if isinstance(args, tuple) or isinstance(args, list): 372 for k in args: 373 self._children.append(k) 374 else: 375 # Let the child resolver step handle it 376 # Applies to iterables, callables, literal elements 377 self._children.append(args)
This method appends one or more elements to the list of children under this element.
In order to facilitate fluid tree construction, it accepts:
- A single child element, like a text string or another element
- A callable that returns a valid child when called
- A list, tuple, or iterable of child elements or callables
- n positional arguments which may be a mix of child elements, callables, or iterables
This method is the backbone for the [] syntax.
Parameters
child_or_childs:
Any acceptable child including iterables and callables.
379 def deferred_resolve( 380 self, parent: ElementBase | None = None 381 ) -> Generator[Node, None, None]: 382 """ 383 Resolve all attributes and children of the HTML element, except for callable children. 384 385 This method performs the following steps: 386 1. Resolves all attributes of the element. 387 2. Resolves all non-callable children. 388 3. Applies context hooks if applicable. 389 4. Generates the HTML string representation of the element. 390 391 Returns: 392 Generator[str, None, None]: A generator that yields strings representing 393 parts of the HTML element. These parts include: 394 - The opening tag with attributes 395 - The content (children) of the element 396 - The closing tag 397 398 Note: 399 - For void elements, only the self-closing tag is yielded. 400 - Callable children are not resolved in this method. 401 """ 402 403 # attrs is a defaultdict of strings. 404 # The key is the attr name, the value is the attr value unescaped. 405 attrs = self.attrs 406 407 children = None 408 409 if not self.is_void_element: 410 children = (child for child in self._resolve_tree(parent)) 411 412 # join_attrs has a configurable lru_cache 413 join_attrs = self.get_attr_join() 414 415 # Generate the key="value" pairs for the attributes 416 # The value escape step lives here because we trust no 417 # previous step in the pipeline. 418 # Magic: Security: Escape all attr values 419 attr_string = " ".join( 420 (join_attrs(k, escape_text(v)) for k, v in attrs.items()) 421 ) 422 423 if self.is_void_element: 424 if attr_string: 425 yield f"<{self.tag} {attr_string}/>" 426 else: 427 yield f"<{self.tag}/>" 428 else: 429 if attr_string: 430 yield f"<{self.tag} {attr_string}>" 431 else: 432 yield f"<{self.tag}>" 433 if children is not None: 434 yield from children 435 yield f"</{self.tag}>"
Resolve all attributes and children of the HTML element, except for callable children.
This method performs the following steps:
- Resolves all attributes of the element.
- Resolves all non-callable children.
- Applies context hooks if applicable.
- Generates the HTML string representation of the element.
Returns: Generator[str, None, None]: A generator that yields strings representing parts of the HTML element. These parts include: - The opening tag with attributes - The content (children) of the element - The closing tag
Note: - For void elements, only the self-closing tag is yielded. - Callable children are not resolved in this method.
437 def resolve( 438 self, parent: ElementBase | None = None 439 ) -> Generator[str, None, None]: 440 """ 441 Generate the flat HTML [string] iterator for the HTML element 442 """ 443 resolver = self.deferred_resolve(parent) 444 for element in resolver: 445 if callable(element): 446 # Feature: nested calling similar to a functional programming style 447 yield from self._resolve_child( 448 element, call_callables=True, parent=parent 449 ) 450 else: 451 yield cast(str, element)
Generate the flat HTML [string] iterator for the HTML element
453 def render(self, parent: ElementBase | None = None) -> str: 454 """ 455 Render the HTML element 456 """ 457 return "".join(self.resolve(parent))
Render the HTML element