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()
SPECIAL_ATTRS = {'class': <function GlobalAttrs.class_>, 'style': <function GlobalAttrs.style>}
class ElementMeta(abc.ABCMeta):
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

class BaseElement(html_compose.base_types.ElementBase):
 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

BaseElement( tag: str, void_element: bool = False, attrs: Union[Iterable[html_compose.base_attribute.BaseAttribute], Mapping[str, Union[NoneType, str, float, int, bool, Iterable[str | float | int | bool], Mapping[str | float | int | bool, bool]]], Iterable[Union[html_compose.base_attribute.BaseAttribute, Iterable[html_compose.base_attribute.BaseAttribute], Mapping[str, Union[NoneType, str, float, int, bool, Iterable[str | float | int | bool], Mapping[str | float | int | bool, bool]]]]], NoneType] = None, children: list | None = None)
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.

tag: str
attrs: dict[str, str]
is_void_element: bool
def append( self, *child_or_childs: Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]):
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.

def deferred_resolve( self, parent: html_compose.base_types.ElementBase | None = None) -> Generator[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]], NoneType, NoneType]:
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:

  1. Resolves all attributes of the element.
  2. Resolves all non-callable children.
  3. Applies context hooks if applicable.
  4. 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.

def resolve( self, parent: html_compose.base_types.ElementBase | None = None) -> Generator[str, NoneType, NoneType]:
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

def render(self, parent: html_compose.base_types.ElementBase | None = None) -> str:
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