Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
5d44087
Add simple copy method to allow simulating removing placeholders.
ianjosephwilson Jun 11, 2026
4cb2111
Typeguard against bogus empty starttag_text.
ianjosephwilson Jun 11, 2026
28c6198
Add debugging/introspection info to open tags.
ianjosephwilson Jun 11, 2026
45fbef7
Improve unclosed tags message for ambiguous slash case.
ianjosephwilson Jun 11, 2026
181ee6b
Fix method defn order.
ianjosephwilson Jun 12, 2026
fcb9f4c
Always fallback to tag str for error, fixes typecheck.
ianjosephwilson Jun 12, 2026
8b0638e
Refine error messages for other cases with trailing slash is consumed…
ianjosephwilson Jun 12, 2026
2a1f10a
Use getter directly but still guard against None.
ianjosephwilson Jun 12, 2026
8c617dc
Restrict self-close suggestion to components.
ianjosephwilson Jun 12, 2026
c1b0132
Restore component start/tag mismatch test.
ianjosephwilson Jun 15, 2026
42dcdb2
Add tmap and format_endtag for error reporting.
ianjosephwilson Jun 15, 2026
d0decf4
Be more surgical about catching nested component error.
ianjosephwilson Jun 15, 2026
1dc72ed
Clump parse info into class.
ianjosephwilson Jun 15, 2026
0d7a0a4
Cut this out for now.
ianjosephwilson Jun 15, 2026
ae65b27
Fold parse info tracking into source tracker.
ianjosephwilson Jun 16, 2026
1d7a8ad
Backport more consistent parser position and source info tracking.
ianjosephwilson Jun 24, 2026
e47303c
Formatting.
ianjosephwilson Jun 24, 2026
89b052a
Actually store positions on other elements.
ianjosephwilson Jun 24, 2026
4f5077a
Test that the parser position is actually being set on the nodes.
ianjosephwilson Jun 24, 2026
2abcb85
Fold loop into test as tempfix for type issues around parser_pos.
ianjosephwilson Jun 24, 2026
d462bd4
Coerce SourceTracker into Iterator and consolidate placeholder tracki…
ianjosephwilson Jun 24, 2026
f85ab7c
Pull the placeholder config definition up to processor to keep parser…
ianjosephwilson Jun 24, 2026
52d9edd
Add TTree to wrap root TNode coming from parser with metadata.
ianjosephwilson Jun 24, 2026
b7ad50f
Add helper method to unpack sinfos tuple into a mapping.
ianjosephwilson Jun 25, 2026
c5fa872
Draft of adding custom exceptions.
ianjosephwilson Jun 26, 2026
4eabdd2
use regular assertion error for what would be our own bug
ianjosephwilson Jun 27, 2026
04ff538
Update parser exception tests.
ianjosephwilson Jun 27, 2026
0caca2e
Update processor test exceptions.
ianjosephwilson Jun 27, 2026
809fda7
Add draft source reader for error reporting.
ianjosephwilson Jun 28, 2026
48756cc
Add some parser error reporting examples.
ianjosephwilson Jun 28, 2026
2e2bed4
Add processor error reporting example.
ianjosephwilson Jun 28, 2026
93fd99a
Fix test for now.
ianjosephwilson Jun 28, 2026
26be511
Use the starttag ref directly to compute start of children body.
ianjosephwilson Jul 1, 2026
18b90e4
Use proxy methods on SourceTracker.
ianjosephwilson Jul 1, 2026
193f613
Use TemplateRef instead of str for starttag_text.
ianjosephwilson Jul 1, 2026
0ef7146
Use TemplateRef for attrs in source info.
ianjosephwilson Jul 1, 2026
48dfdf1
Rein in placeholders with placeholder-independent PartPosition.
ianjosephwilson Jul 2, 2026
b28e4db
Try to reduce position translation complexity.
ianjosephwilson Jul 3, 2026
90de493
Add ambiguous slash checks for normal elements as well.
ianjosephwilson Jul 4, 2026
2b348e0
Simplify
ianjosephwilson Jul 4, 2026
0a32435
Drop unused method.
ianjosephwilson Jul 4, 2026
e915387
Use chain.from_iterable instead of sum.
ianjosephwilson Jul 4, 2026
715c359
Add note to docs.
ianjosephwilson Jul 4, 2026
d49af2a
Refactor error display and expand type handling.
ianjosephwilson Jul 4, 2026
a8dcb87
Add processing error handling tests.
ianjosephwilson Jul 5, 2026
edf0c09
Cleanup logic a bit.
ianjosephwilson Jul 5, 2026
d1a1e9c
Bring parser pos translator into parser itself.
ianjosephwilson Jul 5, 2026
a6bf918
Add fragment test.
ianjosephwilson Jul 5, 2026
d6aea2a
Formatting.
ianjosephwilson Jul 5, 2026
8296c56
Move values match to reader and cleanup iteration in tracker.
ianjosephwilson Jul 5, 2026
3e9492d
Don't go to the next part if there was a NL.
ianjosephwilson Jul 6, 2026
2da2b75
Include comment 'tags' in error message.
ianjosephwilson Jul 6, 2026
e7d0263
Move slice into TemplateRef and start proper tests.
ianjosephwilson Jul 6, 2026
cf2cddb
Add a validation check to make sure positions are logical.
ianjosephwilson Jul 7, 2026
dbf31f5
Expand position translator coverage.
ianjosephwilson Jul 7, 2026
7bc2728
sp
ianjosephwilson Jul 7, 2026
ea37e69
Improve messaging when tags are not closed.
ianjosephwilson Jul 7, 2026
ccfb151
Update comment.
ianjosephwilson Jul 8, 2026
0d3663f
Expand component invocation testing.
ianjosephwilson Jul 8, 2026
8047cda
Test internal function/factory component error.
ianjosephwilson Jul 8, 2026
f92b10d
Check internal element attribute error.
ianjosephwilson Jul 8, 2026
f77723d
Convert unified index to normal interpolations index.
ianjosephwilson Jul 8, 2026
cfacc40
Expand test coverage for TemplateRef.slice.
ianjosephwilson Jul 8, 2026
fd35ec7
Add tests for part position to line position conversion
ianjosephwilson Jul 8, 2026
b0ff3d7
Disable formatter for whitespace tests.
ianjosephwilson Jul 8, 2026
b26165d
Remove dependency on the actively tracked template index.
ianjosephwilson Jul 12, 2026
64b668d
Fix ty warning for now.
ianjosephwilson Jul 12, 2026
e2f55fb
Drop ref attrs until we need them.
ianjosephwilson Jul 15, 2026
657e7d1
Fix first==last strings bug and interpolations bug, improve clarity.
ianjosephwilson Jul 26, 2026
fd07cae
Import from true origin.
ianjosephwilson Jul 26, 2026
2a1ef8c
Move unified index handling into template_utils.
ianjosephwilson Jul 26, 2026
3220d37
Correct test, use function.
ianjosephwilson Jul 26, 2026
cecb384
Use unified indexing in tref iter and group iter tests.
ianjosephwilson Jul 26, 2026
a02bf9f
Add TemplateRef.parts_iter that includes empty strings.
ianjosephwilson Jul 26, 2026
d223853
Expand tests to cover same start and stop for both a string and inter…
ianjosephwilson Jul 26, 2026
a9520c4
Drop the walrus.
ianjosephwilson Jul 26, 2026
857d2dd
Insource combining trefs and expand tests.
ianjosephwilson Jul 26, 2026
f9459ca
Simplify parser pos translator by precomputing and preconfiguring mor…
ianjosephwilson Jul 28, 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
668 changes: 487 additions & 181 deletions tdom/parser.py

Large diffs are not rendered by default.

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

import pytest

from .parser import TemplateParser
from .parser import (
AttributeParsingError,
ParsingError,
TemplateParser,
)
from .placeholders import make_placeholder_config
from .template_utils import TemplateRef
from .template_utils import PartPosition, TemplateRef
from .tnodes import (
TComment,
TComponent,
Expand Down Expand Up @@ -208,17 +212,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 +246,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 +342,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 +395,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 +454,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 +518,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 +633,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