Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tdom/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,6 @@ def __str__(self) -> str:
return f"<{self.tag}{attrs_str}></{self.tag}>"
children_str = self._children_to_str()
return f"<{self.tag}{attrs_str}>{children_str}</{self.tag}>"


type ParentNode = Element | Fragment
23 changes: 10 additions & 13 deletions tdom/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
TSpreadAttribute,
TTemplatedAttribute,
TText,
TParentNode,
)

type HTMLAttribute = tuple[str, str | None]
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

# ------------------------------------------
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions tdom/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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&apos;s")
assert node == TText.literal("Panini's")
assert node == TFragment(children=(TText.literal("Panini's"),))


def test_parse_void_element():
Expand Down Expand Up @@ -88,12 +88,12 @@ def test_parse_element_attribute_order():

def test_parse_comment():
node = TemplateParser.parse(t"<!-- This is a comment -->")
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"<!DOCTYPE html>")
assert node == TDocumentType("html")
assert node == TFragment(children=(TDocumentType("html"),))


def test_parse_multiple_voids():
Expand Down
15 changes: 10 additions & 5 deletions tdom/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,6 +27,7 @@
TSpreadAttribute,
TTemplatedAttribute,
TText,
TParentNode,
)
from .placeholders import TemplateRef
from .template_utils import template_from_parts
Expand All @@ -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)


Expand Down Expand Up @@ -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
10 changes: 5 additions & 5 deletions tdom/processor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<!--This is a comment-->")
assert node == Comment("This is a comment")
assert node == Fragment(children=[Comment("This is a comment")])
assert str(node) == "<!--This is a comment-->"


def test_parse_document_type():
node = html(t"<!doctype html>")
assert node == DocumentType("html")
assert node == Fragment(children=[DocumentType("html")])
assert str(node) == "<!DOCTYPE html>"


Expand Down Expand Up @@ -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"


Expand Down Expand Up @@ -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!"


Expand Down
2 changes: 1 addition & 1 deletion tdom/tnodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,4 @@ class TComponent(TNode):
children: tuple[TNode, ...] = field(default_factory=tuple)


type TTag = TElement | TComponent | TFragment
type TParentNode = TElement | TComponent | TFragment