Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
6e295f9
Add simple copy method to allow simulating removing placeholders.
ianjosephwilson Jun 11, 2026
4077f3e
Typeguard against bogus empty starttag_text.
ianjosephwilson Jun 11, 2026
b95eaa8
Add debugging/introspection info to open tags.
ianjosephwilson Jun 11, 2026
9084150
Improve unclosed tags message for ambiguous slash case.
ianjosephwilson Jun 11, 2026
8b226fc
Fix method defn order.
ianjosephwilson Jun 12, 2026
f1b5739
Always fallback to tag str for error, fixes typecheck.
ianjosephwilson Jun 12, 2026
922c2bf
Refine error messages for other cases with trailing slash is consumed…
ianjosephwilson Jun 12, 2026
2b03b4e
Use getter directly but still guard against None.
ianjosephwilson Jun 12, 2026
a10f209
Restrict self-close suggestion to components.
ianjosephwilson Jun 12, 2026
e21b9bd
Restore component start/tag mismatch test.
ianjosephwilson Jun 15, 2026
75df60e
Add tmap and format_endtag for error reporting.
ianjosephwilson Jun 15, 2026
60095db
Be more surgical about catching nested component error.
ianjosephwilson Jun 15, 2026
323cafc
Clump parse info into class.
ianjosephwilson Jun 15, 2026
d8f98f8
Cut this out for now.
ianjosephwilson Jun 15, 2026
e3b71a7
Fold parse info tracking into source tracker.
ianjosephwilson Jun 16, 2026
4e3def3
Backport more consistent parser position and source info tracking.
ianjosephwilson Jun 24, 2026
e9a58d9
Formatting.
ianjosephwilson Jun 24, 2026
28e34e0
Actually store positions on other elements.
ianjosephwilson Jun 24, 2026
5dcc189
Test that the parser position is actually being set on the nodes.
ianjosephwilson Jun 24, 2026
0e0a842
Fold loop into test as tempfix for type issues around parser_pos.
ianjosephwilson Jun 24, 2026
9f2fcab
Coerce SourceTracker into Iterator and consolidate placeholder tracki…
ianjosephwilson Jun 24, 2026
cae9296
Pull the placeholder config definition up to processor to keep parser…
ianjosephwilson Jun 24, 2026
b3d50a7
Add TTree to wrap root TNode coming from parser with metadata.
ianjosephwilson Jun 24, 2026
2d92ab7
Add helper method to unpack sinfos tuple into a mapping.
ianjosephwilson Jun 25, 2026
e452fdc
Draft of adding custom exceptions.
ianjosephwilson Jun 26, 2026
a4fe362
use regular assertion error for what would be our own bug
ianjosephwilson Jun 27, 2026
787704a
Update parser exception tests.
ianjosephwilson Jun 27, 2026
5f73f1f
Update processor test exceptions.
ianjosephwilson Jun 27, 2026
25161f0
Add draft source reader for error reporting.
ianjosephwilson Jun 28, 2026
fd05b3f
Add some parser error reporting examples.
ianjosephwilson Jun 28, 2026
83a1edb
Add processor error reporting example.
ianjosephwilson Jun 28, 2026
ce1efd1
Fix test for now.
ianjosephwilson Jun 28, 2026
494733c
Use the starttag ref directly to compute start of children body.
ianjosephwilson Jul 1, 2026
05c2f43
Use proxy methods on SourceTracker.
ianjosephwilson Jul 1, 2026
20902f8
Use TemplateRef instead of str for starttag_text.
ianjosephwilson Jul 1, 2026
33bdec0
Use TemplateRef for attrs in source info.
ianjosephwilson Jul 1, 2026
28f8383
Rein in placeholders with placeholder-independent PartPosition.
ianjosephwilson Jul 2, 2026
b61e302
Try to reduce position translation complexity.
ianjosephwilson Jul 3, 2026
34c79a0
Add ambiguous slash checks for normal elements as well.
ianjosephwilson Jul 4, 2026
5896d28
Simplify
ianjosephwilson Jul 4, 2026
4c8e650
Drop unused method.
ianjosephwilson Jul 4, 2026
dec74ff
Use chain.from_iterable instead of sum.
ianjosephwilson Jul 4, 2026
2e497d5
Add note to docs.
ianjosephwilson Jul 4, 2026
ec561af
Refactor error display and expand type handling.
ianjosephwilson Jul 4, 2026
ce5b448
Add processing error handling tests.
ianjosephwilson Jul 5, 2026
d35a922
Cleanup logic a bit.
ianjosephwilson Jul 5, 2026
eba1f60
Bring parser pos translator into parser itself.
ianjosephwilson Jul 5, 2026
b38441b
Add fragment test.
ianjosephwilson Jul 5, 2026
0071ef6
Formatting.
ianjosephwilson Jul 5, 2026
503a813
Move values match to reader and cleanup iteration in tracker.
ianjosephwilson Jul 5, 2026
d9481ec
Don't go to the next part if there was a NL.
ianjosephwilson Jul 6, 2026
fe0068b
Include comment 'tags' in error message.
ianjosephwilson Jul 6, 2026
735c838
Move slice into TemplateRef and start proper tests.
ianjosephwilson Jul 6, 2026
0b38568
Add a validation check to make sure positions are logical.
ianjosephwilson Jul 7, 2026
e88391d
Expand position translator coverage.
ianjosephwilson Jul 7, 2026
6d3337d
sp
ianjosephwilson Jul 7, 2026
ad16463
Improve messaging when tags are not closed.
ianjosephwilson Jul 7, 2026
8064abe
Update comment.
ianjosephwilson Jul 8, 2026
aadd114
Expand component invocation testing.
ianjosephwilson Jul 8, 2026
d880b3d
Test internal function/factory component error.
ianjosephwilson Jul 8, 2026
51bc6aa
Check internal element attribute error.
ianjosephwilson Jul 8, 2026
10923eb
Convert unified index to normal interpolations index.
ianjosephwilson Jul 8, 2026
97be526
Expand test coverage for TemplateRef.slice.
ianjosephwilson Jul 8, 2026
4d48e87
Add tests for part position to line position conversion
ianjosephwilson Jul 8, 2026
b3e62cd
Disable formatter for whitespace tests.
ianjosephwilson Jul 8, 2026
fa70f30
Remove dependency on the actively tracked template index.
ianjosephwilson Jul 12, 2026
42789eb
Fix ty warning for now.
ianjosephwilson Jul 12, 2026
aa38682
Drop ref attrs until we need them.
ianjosephwilson Jul 15, 2026
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
667 changes: 487 additions & 180 deletions tdom/parser.py

Large diffs are not rendered by default.

197 changes: 176 additions & 21 deletions tdom/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

import pytest

from .parser import TemplateParser
from .parser import (
AttributeParsingError,
ParsingError,
TemplateParser,
)
from .placeholders import make_placeholder_config
from .source import PartPosition
from .template_utils import TemplateRef
from .tnodes import (
TComment,
Expand Down Expand Up @@ -208,17 +213,17 @@ def test_parse_title_unusual():


def test_parse_mismatched_tags():
with pytest.raises(ValueError):
with pytest.raises(ParsingError, match="Mismatch"):
_ = TemplateParser.parse(t"<div><span>Mismatched</div></span>")


def test_parse_unclosed_tag():
with pytest.raises(ValueError):
def test_parse_unclosed_element():
with pytest.raises(ParsingError, match="unclosed tag <div>"):
_ = TemplateParser.parse(t"<div>Unclosed")


def test_parse_unexpected_closing_tag():
with pytest.raises(ValueError):
with pytest.raises(ParsingError, match="Unexpected closing tag"):
_ = TemplateParser.parse(t"Unopened</div>")


Expand All @@ -242,12 +247,12 @@ def test_nested_self_closing_tags():


def test_self_closing_tags_unexpected_closing_tag():
with pytest.raises(ValueError):
with pytest.raises(ParsingError, match="Unexpected closing tag"):
_ = TemplateParser.parse(t"<div /></div>")


def test_self_closing_void_tags_unexpected_closing_tag():
with pytest.raises(ValueError):
with pytest.raises(ParsingError, match="Unexpected closing tag"):
_ = TemplateParser.parse(t"<input /></input>")


Expand Down Expand Up @@ -338,20 +343,28 @@ def test_spread_attr():


def test_templated_attribute_name_error():
with pytest.raises(ValueError):
with pytest.raises(
AttributeParsingError,
match="cannot contain interpolations if the value is also interpolated",
):
attr_name = "some-attr"
_ = TemplateParser.parse(t'<div {attr_name}="value" />')


def test_templated_attribute_name_and_value_error():
with pytest.raises(ValueError):
with pytest.raises(
AttributeParsingError,
match="cannot contain interpolations if the value is also interpolated",
):
attr_name = "some-attr"
value = "value"
_ = TemplateParser.parse(t'<div {attr_name}="{value}" />')


def test_adjacent_spread_attrs_error():
with pytest.raises(ValueError):
with pytest.raises(
AttributeParsingError, match="must have exactly one interpolation in the name"
):
attrs1 = {}
attrs2 = {}
_ = TemplateParser.parse(t"<div {attrs1}{attrs2} />")
Expand Down Expand Up @@ -383,14 +396,16 @@ def test_parse_doctype():

def test_parse_doctype_interpolation_error():
extra = "SYSTEM"
with pytest.raises(ValueError):
with pytest.raises(
ParsingError, match="Interpolations are not allowed in declarations"
):
_ = TemplateParser.parse(t"<!DOCTYPE html {extra}>")


def test_unsupported_decl_error():
with pytest.raises(NotImplementedError):
with pytest.raises(ParsingError, match="Only well formed DOCTYPE declarations"):
_ = TemplateParser.parse(t"<!doctype-alt html500>") # Unknown declaration
with pytest.raises(NotImplementedError):
with pytest.raises(ParsingError, match="Only well formed DOCTYPE declarations"):
_ = TemplateParser.parse(t"<!doctype>") # missing DTD


Expand Down Expand Up @@ -440,34 +455,51 @@ def test_component_element_invalid_closing_tag():
def Component():
pass

with pytest.raises(ValueError):
with pytest.raises(ParsingError, match="Mismatched closing tag </div>"):
_ = TemplateParser.parse(t"<{Component}></div>")


def test_component_element_invalid_opening_tag():
def Component():
pass

with pytest.raises(ValueError):
# @NOTE: intentional expression
with pytest.raises(ParsingError, match="Mismatched closing tag </{Component}>"):
_ = TemplateParser.parse(t"<div></{Component}>")


def test_adjacent_start_component_tag_error():
def Component():
pass

with pytest.raises(ValueError):
with pytest.raises(ParsingError, match="must have exactly one interpolation"):
_ = TemplateParser.parse(t"<{Component}{Component}></{Component}>")


def test_adjacent_end_component_tag_error():
def Component():
pass

with pytest.raises(ValueError):
with pytest.raises(ParsingError, match="must have exactly one interpolation"):
_ = TemplateParser.parse(t"<{Component}></{Component}{Component}>")


def test_unmatched_end_component_tag_error():
def Component():
pass

with pytest.raises(ParsingError, match="Unexpected closing tag </{Component}>"):
_ = TemplateParser.parse(t"</{Component}>")


def test_unclosed_component_tag_error():
def Component():
pass

with pytest.raises(ParsingError, match="unclosed tag <{Component}>"):
_ = TemplateParser.parse(t"<{Component}>")


def test_placeholder_collision_avoidance():
config = make_placeholder_config()
# This test is to ensure that our placeholder detection avoids collisions
Expand All @@ -487,17 +519,17 @@ def test_placeholder_collision_avoidance():

class TestIncompleteParsing:
def test_dangling_quotes(self):
with pytest.raises(ValueError, match="Parser expects more data"):
with pytest.raises(ParsingError, match="Parser expects more data"):
_ = TemplateParser.parse(t"<div a='")
with pytest.raises(ValueError, match="Parser expects more data"):
with pytest.raises(ParsingError, match="Parser expects more data"):
_ = TemplateParser.parse(t'<div a="')

def test_unfinished_attribute(self):
with pytest.raises(ValueError, match="Parser expects more data"):
with pytest.raises(ParsingError, match="Parser expects more data"):
_ = TemplateParser.parse(t"<div a=")

def test_placeholder_missing_from_dangling_quote(self):
with pytest.raises(ValueError, match="Parser expects more data"):
with pytest.raises(ParsingError, match="Parser expects more data"):
_ = TemplateParser.parse(t'<div a="{None}')


Expand Down Expand Up @@ -602,3 +634,126 @@ def test_extract_with_templated_attr_gt_char(self, Component):
strings=("<div>Hello, World!</div>",), i_indexes=()
),
)


class TestElementWithAmbiguousSlash:
def test_root_unclosed_error(self):
with pytest.raises(
ParsingError, match="Did you mean to quote the last attribute.*attr[=]root/"
):
_ = TemplateParser.parse(t"<div attr=root/>")

def test_nested_unclosed_error(self):
with pytest.raises(
ParsingError,
match="Did you mean to quote the last attribute.*attr[=]nested/",
):
_ = TemplateParser.parse(t"<div><div attr=nested/></div>")

def test_double_nested_unclosed_error(self):
with pytest.raises(
ParsingError,
match="Did you mean to quote the last attribute.*attr[=]nested/",
):
_ = TemplateParser.parse(t"<div><div/><div><div attr=nested/></div></div>")

def test_mismatch_with_element_error(self):
with pytest.raises(
ParsingError,
match="Did you mean to quote the last attribute.*attr[=]mismatch/",
):
_ = TemplateParser.parse(t"<section><div attr=mismatch/></section>")

def test_mismatch_with_component_error(self):
def Comp(children: Template) -> Template:
return t""

with pytest.raises(
ParsingError,
match="Did you mean to quote the last attribute.*attr[=]mismatch/",
):
_ = TemplateParser.parse(t"<{Comp}><div attr=mismatch/></{Comp}>")


class TestComponentWithAmbiguousSlash:
@pytest.fixture
def Comp1(self):
def _Comp1(children: Template, title: str) -> Template:
return children

return _Comp1

@pytest.fixture
def Comp2(self):
def _Comp2(children: Template, title: str) -> Template:
return children

return _Comp2

@pytest.fixture
def Comp3(self):
def _Comp3(children: Template, title: str) -> Template:
return children

return _Comp3

def test_mismatch_with_element_error(self, Comp1):
with pytest.raises(
ParsingError,
match="Did you mean to quote the last attribute.*title[=]today/",
):
_ = TemplateParser.parse(t"<div><{Comp1} title=today/></div>")

def test_root_unclosed_error(self, Comp1):
with pytest.raises(
ParsingError,
match="Did you mean to quote the last attribute.*title[=]today/",
):
_ = TemplateParser.parse(t"<{Comp1} title=today/>")

def test_single_nested_unclosed_error(self, Comp1, Comp2):
with pytest.raises(
ParsingError,
match="Did you mean to quote the last attribute.*title[=]today/",
):
_ = TemplateParser.parse(t"<{Comp2}><{Comp1} title=today/></{Comp2}>")

def test_double_nested_unclosed_error(self, Comp1, Comp2, Comp3):
with pytest.raises(
ParsingError,
match="Did you mean to quote the last attribute.*title[=]today/",
):
_ = TemplateParser.parse(
t"<{Comp2}><{Comp1}><{Comp3} title=today/></{Comp1}></{Comp2}>"
)


class TestSourcePosition:
def test_tnode_source_position(self):
"Check that non-fragments are assigned a source position."

def PositionComp() -> Template:
return t""

for tnode_type, fragment in (
(TElement, t"<span></span>"),
(TComment, t"<!--ok-->"),
(TDocumentType, t"<!doctype html>"),
(TComponent, t"<{PositionComp}></{PositionComp}>"),
(TText, t"Just a simple text."),
):
tnode = TemplateParser.parse(t"<div>" + fragment + t"</div>")
assert (
isinstance(tnode, TElement)
and tnode.tag == "div"
and len(tnode.children) == 1
)
el = tnode.children[0]
assert isinstance(el, tnode_type)
assert el.source_pos == PartPosition(index=0, offset=len("<div>"))

def test_fragment_source_position(self):
"Fragments do not have a position right now."
root = TemplateParser.parse(t"<div></div><section></section>")
assert isinstance(root, TFragment)
assert not root.source_pos
Loading
Loading