diff --git a/tdom/nodes.py b/tdom/nodes.py index 346f97f..a50c3c7 100644 --- a/tdom/nodes.py +++ b/tdom/nodes.py @@ -141,3 +141,6 @@ def __str__(self) -> str: return f"<{self.tag}{attrs_str}>" children_str = self._children_to_str() return f"<{self.tag}{attrs_str}>{children_str}" + + +type ParentNode = Element | Fragment diff --git a/tdom/parser.py b/tdom/parser.py index aef287c..eb86f88 100644 --- a/tdom/parser.py +++ b/tdom/parser.py @@ -18,6 +18,7 @@ TSpreadAttribute, TTemplatedAttribute, TText, + TParentNode, ) type HTMLAttribute = tuple[str, str | None] @@ -189,7 +190,7 @@ def make_open_tag(self, tag: str, attrs: t.Sequence[HTMLAttribute]) -> OpenTag: def finalize_tag( self, open_tag: OpenTag, endtag_i_index: int | None = None - ) -> TNode: + ) -> TParentNode: """Finalize an OpenTag into a TNode.""" match open_tag: case OpenTElement(tag=tag, attrs=attrs, children=children): @@ -313,21 +314,17 @@ def close(self) -> None: # Getting the parsed node tree # ------------------------------------------ - def get_tnode(self) -> TNode: + def get_tnode(self) -> TParentNode: """Get the Node tree parsed from the input HTML.""" - # TODO: consider always returning a TTag? - if len(self.root.children) > 1: - # The parse structure results in multiple root elements, so we - # return a Fragment to hold them all. + if len(self.root.children) != 1: + # len > 1: use fragment to hold multiple elements + # len == 0: use fragment return self.finalize_tag(self.root) - elif len(self.root.children) == 1: - # The parse structure results in a single root element, so we - # return that element directly. This will be a non-Fragment Node. + elif isinstance(self.root.children[0], (TFragment, TElement, TComponent)): + # len == 1 and only child is a (potential) container: return child return self.root.children[0] else: - # Special case: the parse structure is empty; we treat - # this as an empty document fragment. - # CONSIDER: or as an empty text node? + # len == 1 and non-container: use fragment return self.finalize_tag(self.root) # ------------------------------------------ @@ -353,7 +350,7 @@ def feed_template(self, template: Template) -> None: self.feed_str(template.strings[-1]) @staticmethod - def parse(t: Template) -> TNode: + def parse(t: Template) -> TParentNode: """ Parse a Template containing valid HTML and substitutions and return a TNode tree representing its structure. This cachable structure can later diff --git a/tdom/parser_test.py b/tdom/parser_test.py index a5167b1..ac150c4 100644 --- a/tdom/parser_test.py +++ b/tdom/parser_test.py @@ -23,12 +23,12 @@ def test_parse_empty(): def test_parse_text(): node = TemplateParser.parse(t"Hello, world!") - assert node == TText.literal("Hello, world!") + assert node == TFragment(children=(TText.literal("Hello, world!"),)) def test_parse_text_with_entities(): node = TemplateParser.parse(t"Panini's") - assert node == TText.literal("Panini's") + assert node == TFragment(children=(TText.literal("Panini's"),)) def test_parse_void_element(): @@ -88,12 +88,12 @@ def test_parse_element_attribute_order(): def test_parse_comment(): node = TemplateParser.parse(t"") - assert node == TComment.literal(" This is a comment ") + assert node == TFragment(children=(TComment.literal(" This is a comment "),)) def test_parse_doctype(): node = TemplateParser.parse(t"") - assert node == TDocumentType("html") + assert node == TFragment(children=(TDocumentType("html"),)) def test_parse_multiple_voids(): diff --git a/tdom/processor.py b/tdom/processor.py index e3773a9..4ed7456 100644 --- a/tdom/processor.py +++ b/tdom/processor.py @@ -10,7 +10,7 @@ from .callables import get_callable_info from .format import format_interpolation as base_format_interpolation from .format import format_template -from .nodes import Comment, DocumentType, Element, Fragment, Node, Text +from .nodes import Comment, DocumentType, Element, Fragment, Node, Text, ParentNode from .parser import ( HTMLAttribute, HTMLAttributesDict, @@ -27,6 +27,7 @@ TSpreadAttribute, TTemplatedAttribute, TText, + TParentNode, ) from .placeholders import TemplateRef from .template_utils import template_from_parts @@ -44,7 +45,7 @@ def __html__(self) -> str: ... # pragma: no cover @lru_cache(maxsize=0 if "pytest" in sys.modules else 512) -def _parse_and_cache(cachable: CachableTemplate) -> TNode: +def _parse_and_cache(cachable: CachableTemplate) -> TParentNode: return TemplateParser.parse(cachable.template) @@ -584,8 +585,12 @@ def _resolve_t_node(t_node: TNode, interpolations: tuple[Interpolation, ...]) -> # -------------------------------------------------------------------------- -def html(template: Template) -> Node: - """Parse an HTML t-string, substitue values, and return a tree of Nodes.""" +def html(template: Template) -> ParentNode: + """Parse an HTML t-string, substitute values, and return a tree of Nodes.""" cachable = CachableTemplate(template) t_node = _parse_and_cache(cachable) - return _resolve_t_node(t_node, template.interpolations) + res = _resolve_t_node(t_node, template.interpolations) + if not isinstance(res, (Element, Fragment)): + return Fragment(children=[res]) + else: + return res diff --git a/tdom/processor_test.py b/tdom/processor_test.py index 30a4ed5..acdd236 100644 --- a/tdom/processor_test.py +++ b/tdom/processor_test.py @@ -23,19 +23,19 @@ def test_parse_empty(): def test_parse_text(): node = html(t"Hello, world!") - assert node == Text("Hello, world!") + assert node == Fragment(children=[Text("Hello, world!")]) assert str(node) == "Hello, world!" def test_parse_comment(): node = html(t"") - assert node == Comment("This is a comment") + assert node == Fragment(children=[Comment("This is a comment")]) assert str(node) == "" def test_parse_document_type(): node = html(t"") - assert node == DocumentType("html") + assert node == Fragment(children=[DocumentType("html")]) assert str(node) == "" @@ -1056,7 +1056,7 @@ def Header(): return html(t"{'Hello World'}") node = html(t"<{Header} />") - assert node == Text("Hello World") + assert node == Fragment(children=[Text("Hello World")]) assert str(node) == "Hello World" @@ -1289,7 +1289,7 @@ def test_attribute_type_component(): t"data-false={a_false} data-none={a_none} data-float={a_float} " t"data-dt={a_dt} {spread_attrs}/>" ) - assert node == Text("Looks good!") + assert node == Fragment(children=[Text("Looks good!")]) assert str(node) == "Looks good!" diff --git a/tdom/tnodes.py b/tdom/tnodes.py index d49fc64..8ebda8f 100644 --- a/tdom/tnodes.py +++ b/tdom/tnodes.py @@ -94,4 +94,4 @@ class TComponent(TNode): children: tuple[TNode, ...] = field(default_factory=tuple) -type TTag = TElement | TComponent | TFragment +type TParentNode = TElement | TComponent | TFragment