Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
5a5e34e
Transformer prototype.
ianjosephwilson Nov 26, 2025
d1c800a
Generic escaping function.
ianjosephwilson Nov 26, 2025
4bfd3e3
Remove conditional dispatch at render-time in favor of fixed callback…
ianjosephwilson Dec 1, 2025
ff76c79
Fix typos.
ianjosephwilson Dec 1, 2025
4ac31c5
Add optimization around str.
ianjosephwilson Dec 1, 2025
2cd9537
Add select example.
ianjosephwilson Dec 1, 2025
18b4290
cleanup/docs
ianjosephwilson Dec 1, 2025
cab0f10
Add back iterable interpolation.
ianjosephwilson Dec 26, 2025
3706312
Make these write only so lru_cache on methods of subclasses work.
ianjosephwilson Dec 26, 2025
7c3170f
Add context var handling but we should probably just use a dedicated …
ianjosephwilson Dec 26, 2025
f9b1228
Add context vars proof of concept test but needs more concurrency.
ianjosephwilson Dec 26, 2025
02f82d8
Finish rebase.
ianjosephwilson Dec 31, 2025
e37d228
Expand tests.
ianjosephwilson Jan 1, 2026
8df29bd
Delegate as much attribute resolution to processor as possible.
ianjosephwilson Jan 1, 2026
f862917
Cleanup transformer tests.
ianjosephwilson Jan 1, 2026
23260fb
Use another lru cache.
ianjosephwilson Jan 1, 2026
a3c3ab4
Expand tests for cache, add another large smoketest.
ianjosephwilson Jan 1, 2026
7c41ee1
Fix signature.
ianjosephwilson Jan 1, 2026
5d65cc7
Add __html__ to smoke test.
ianjosephwilson Jan 1, 2026
808a0c3
Add in experiment with callable info style components.
ianjosephwilson Jan 1, 2026
7c93227
Type checking fixes.
ianjosephwilson Jan 4, 2026
b93c4d7
Cleanup attr handling names.
ianjosephwilson Jan 4, 2026
f273e94
Add in a few explainations.
ianjosephwilson Jan 4, 2026
cdf531b
Use clearer name.
ianjosephwilson Jan 13, 2026
dc8b381
Try to push API in the direction of tdom's node api.
ianjosephwilson Jan 14, 2026
b7f2a13
Actually use the escape function.
ianjosephwilson Jan 15, 2026
4ff937e
Use call info style.
ianjosephwilson Jan 15, 2026
216bfc0
Stop using cinfo to determine if context values will be returned.
ianjosephwilson Jan 15, 2026
f83e63b
Match other calls.
ianjosephwilson Jan 15, 2026
620bcf2
Add more comments to tests and make more idiomatic with processor.
ianjosephwilson Jan 15, 2026
1665336
Call embedded template a children template to avoid confusion.
ianjosephwilson Jan 15, 2026
5074572
Fix indent error.
ianjosephwilson Jan 15, 2026
4b0ac3f
Add some common escape tests.
ianjosephwilson Jan 15, 2026
3932447
After switching from yield it seems we don't need to haul around the …
ianjosephwilson Jan 15, 2026
9d5bc12
Formatting.
ianjosephwilson Jan 15, 2026
8bbc0d1
Stop pre-processing the component template and just act on return value.
ianjosephwilson Jan 15, 2026
aad2364
Start cleaning comments.
ianjosephwilson Jan 15, 2026
1a4bd02
Stop proxying transform api and just call it directly.
ianjosephwilson Jan 15, 2026
6e337fb
Draft of using types.
ianjosephwilson Jan 16, 2026
57f73ac
Use existing style escaping.
ianjosephwilson Jan 16, 2026
6f3049b
Use interpolation format if present.
ianjosephwilson Jan 16, 2026
4b32239
Re-org to optimize native strs.
ianjosephwilson Jan 16, 2026
8545045
Don't omit False, just render it to cleanup typing-story.
ianjosephwilson Jan 16, 2026
7307024
Refactor component kwargs preparation to make it reusable.
ianjosephwilson Jan 17, 2026
9d99422
Use processor component kwargs prep.
ianjosephwilson Jan 17, 2026
285ab75
Apply second call to finish invoking class components.
ianjosephwilson Jan 19, 2026
18a7576
Add class component example and add runtime checks to component retur…
ianjosephwilson Jan 19, 2026
ef6b0df
Fix url in test example.
ianjosephwilson Jan 19, 2026
47ea44a
Use inspect.is_class to build then call class components.
ianjosephwilson Jan 19, 2026
1b5d8b2
Document component return value.
ianjosephwilson Jan 19, 2026
08d543a
Make context_values typing work with uv.
ianjosephwilson Jan 19, 2026
32adb68
Make name more consistent.
ianjosephwilson Jan 21, 2026
e2662d5
Try to refactor a unified invoke component.
ianjosephwilson Jan 21, 2026
591a15f
Fix typing.
ianjosephwilson Jan 21, 2026
0fe98bd
Format.
ianjosephwilson Jan 21, 2026
5e30250
Use resolve_text_without_recursion as top-level function.
ianjosephwilson Jan 23, 2026
c7b7185
Formatting.
ianjosephwilson Jan 23, 2026
cae1a50
Handle edge cases around html pedantry.
ianjosephwilson Jan 24, 2026
b4448b3
Format
ianjosephwilson Jan 24, 2026
1df9b58
Split invoke by strategy to try to cleanup typing.
ianjosephwilson Jan 25, 2026
747cbca
Remove unused import.
ianjosephwilson Jan 26, 2026
a93f732
Make text within certain tags more explicit.
ianjosephwilson Jan 26, 2026
6455889
Add docstring.
ianjosephwilson Jan 26, 2026
442cd97
Cleanup more comments.
ianjosephwilson Jan 26, 2026
cb35ea7
Add simple foreign element tests to see where things are at.
ianjosephwilson Jan 26, 2026
005b4af
Minor cleanup.
ianjosephwilson Jan 26, 2026
b4077cb
Use pytest parametrize instead of forloop.
ianjosephwilson Jan 26, 2026
f3fa3a6
Use markup to optionally bypass script, comment and style escape func…
ianjosephwilson Jan 26, 2026
faa15b8
Remove general purpose escape function and use explict functions.
ianjosephwilson Jan 26, 2026
0903a0f
Move hasHTMLDunder into protocols.
ianjosephwilson Jan 26, 2026
85b9a04
Use has html protocol instead of hasattr check.
ianjosephwilson Jan 26, 2026
51e1a6d
Update tests.
ianjosephwilson Jan 26, 2026
9705382
Cleanup special case escaping.
ianjosephwilson Jan 26, 2026
e70e16c
Add system provided context example.
ianjosephwilson Jan 27, 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
19 changes: 16 additions & 3 deletions tdom/escaping.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@

from markupsafe import escape as markup_escape

from .protocols import HasHTMLDunder


escape_html_text = markup_escape # unify api for test of project


GT = ">"
LT = "<"


def escape_html_comment(text: str) -> str:
def escape_html_comment(text: str, allow_markup: bool = False) -> str:
"""Escape text injected into an HTML comment."""
if not text:
return text
elif allow_markup and isinstance(text, HasHTMLDunder):
return text.__html__()
elif not allow_markup and type(text) is not str:
# text manipulation triggers regular html escapes on Markup
text = str(text)

# - text must not start with the string ">"
if text[0] == ">":
text = GT + text[1:]
Expand All @@ -39,8 +48,10 @@ def escape_html_comment(text: str) -> str:
STYLE_RES = ((re.compile("</(?P<tagname>style)>", re.I | re.A), LT + r"/\g<tagname>>"),)


def escape_html_style(text: str) -> str:
def escape_html_style(text: str, allow_markup: bool = False) -> str:
"""Escape text injected into an HTML style element."""
if allow_markup and isinstance(text, HasHTMLDunder):
return text.__html__()
for matche_re, replace_text in STYLE_RES:
text = re.sub(matche_re, replace_text, text)
return text
Expand All @@ -62,7 +73,7 @@ def escape_html_style(text: str) -> str:
)


def escape_html_script(text: str) -> str:
def escape_html_script(text: str, allow_markup: bool = False) -> str:
"""
Escape text injected into an HTML script element.

Expand All @@ -75,6 +86,8 @@ def escape_html_script(text: str) -> str:
- "<script" as "\x3cscript"
- "</script" as "\x3c/script"`
"""
if allow_markup and isinstance(text, HasHTMLDunder):
return text.__html__()
for match_re, replace_text in SCRIPT_RES:
text = re.sub(match_re, replace_text, text)
return text
40 changes: 39 additions & 1 deletion tdom/escaping_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
from .escaping import escape_html_comment, escape_html_script, escape_html_style
from markupsafe import Markup

from .escaping import (
escape_html_comment,
escape_html_script,
escape_html_style,
escape_html_text,
)


def test_escape_html_text() -> None:
assert escape_html_text("<div>") == "&lt;div&gt;"


def test_escape_html_comment_empty() -> None:
Expand Down Expand Up @@ -27,6 +38,15 @@ def test_escape_html_comment_ends_with_lt_dash() -> None:
assert escape_html_comment("This is a comment<!-") == "This is a comment&lt;!-"


def test_escape_html_comment_markup() -> None:
input_text = "-->"
escaped_text = "--&gt;"
out = escape_html_comment(Markup(input_text), allow_markup=False)
assert out != input_text and out == escaped_text
out = escape_html_comment(Markup(input_text), allow_markup=True)
assert out == input_text and out != escaped_text


def test_escape_html_style() -> None:
input_text = "body { color: red; }</style> p { font-SIZE: 12px; }</STYLE>"
expected_output = (
Expand All @@ -35,10 +55,28 @@ def test_escape_html_style() -> None:
assert escape_html_style(input_text) == expected_output


def test_escape_html_style_markup() -> None:
input_text = "</STYLE>"
escaped_text = "&lt;/STYLE>"
out = escape_html_style(Markup(input_text), allow_markup=False)
assert out != input_text and out == escaped_text
out = escape_html_style(Markup(input_text), allow_markup=True)
assert out == input_text and out != escaped_text


def test_escape_html_script() -> None:
input_text = "<!-- <script>var a = 1;</script> </SCRIPT>"
expected_output = "\\x3c!-- \\x3cscript>var a = 1;\\x3c/script> \\x3c/SCRIPT>"
assert escape_html_script(input_text) == expected_output
# Smoketest that escaping is working and we are not just escaping back to the same value.
for text in ("<script", "</script", "<!--"):
assert escape_html_script(text) != text


def test_escape_html_script_markup() -> None:
input_text = "<script>"
escaped_text = "\\x3cscript>"
out = escape_html_script(Markup(input_text), allow_markup=False)
assert out != input_text and out == escaped_text
out = escape_html_script(Markup(input_text), allow_markup=True)
assert out == input_text and out != escaped_text
69 changes: 39 additions & 30 deletions tdom/processor.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import sys
import typing as t
from collections.abc import Iterable, Sequence
from collections.abc import Iterable, Sequence, Callable
from functools import lru_cache
from string.templatelib import Interpolation, Template
from dataclasses import dataclass

from markupsafe import Markup

from .callables import get_callable_info
from .callables import get_callable_info, CallableInfo
from .format import format_interpolation as base_format_interpolation
from .format import format_template
from .nodes import Comment, DocumentType, Element, Fragment, Node, Text
Expand All @@ -31,11 +31,7 @@
from .placeholders import TemplateRef
from .template_utils import template_from_parts
from .utils import CachableTemplate, LastUpdatedOrderedDict


@t.runtime_checkable
class HasHTMLDunder(t.Protocol):
def __html__(self) -> str: ... # pragma: no cover
from .protocols import HasHTMLDunder


# TODO: in Ian's original PR, this caching was tethered to the
Expand Down Expand Up @@ -437,6 +433,39 @@ def _kebab_to_snake(name: str) -> str:
return name.replace("-", "_").lower()


def _prep_component_kwargs(
callable_info: CallableInfo,
attrs: AttributesDict,
system_kwargs: dict[str, object],
kebab_to_snake: Callable[[str], str] = _kebab_to_snake,
):
if callable_info.requires_positional:
raise TypeError(
"Component callables cannot have required positional arguments."
)

kwargs: AttributesDict = {}

# Add all supported attributes
for attr_name, attr_value in attrs.items():
snake_name = kebab_to_snake(attr_name)
if snake_name in callable_info.named_params or callable_info.kwargs:
kwargs[snake_name] = attr_value

for attr_name, attr_value in system_kwargs.items():
if attr_name in callable_info.named_params or callable_info.kwargs:
kwargs[attr_name] = attr_value

# Check to make sure we've fully satisfied the callable's requirements
missing = callable_info.required_named_params - kwargs.keys()
if missing:
raise TypeError(
f"Missing required parameters for component: {', '.join(missing)}"
)

return kwargs


def _invoke_component(
attrs: AttributesDict,
children: list[Node], # TODO: why not TNode, though?
Expand Down Expand Up @@ -477,29 +506,9 @@ def _invoke_component(
)
callable_info = get_callable_info(value)

if callable_info.requires_positional:
raise TypeError(
"Component callables cannot have required positional arguments."
)

kwargs: AttributesDict = {}

# Add all supported attributes
for attr_name, attr_value in attrs.items():
snake_name = _kebab_to_snake(attr_name)
if snake_name in callable_info.named_params or callable_info.kwargs:
kwargs[snake_name] = attr_value

# Add children if appropriate
if "children" in callable_info.named_params or callable_info.kwargs:
kwargs["children"] = tuple(children)

# Check to make sure we've fully satisfied the callable's requirements
missing = callable_info.required_named_params - kwargs.keys()
if missing:
raise TypeError(
f"Missing required parameters for component: {', '.join(missing)}"
)
kwargs = _prep_component_kwargs(
callable_info, attrs, system_kwargs={"children": tuple(children)}
)

result = value(**kwargs)
return _node_from_value(result)
Expand Down
6 changes: 6 additions & 0 deletions tdom/protocols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import typing as t


@t.runtime_checkable
class HasHTMLDunder(t.Protocol):
def __html__(self) -> str: ... # pragma: no cover
11 changes: 11 additions & 0 deletions tdom/template_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,14 @@ def __post_init__(self):
raise ValueError(
"TemplateRef must have one more string than interpolation indexes."
)

def __iter__(self):
index = 0
last_s_index = len(self.strings) - 1
while index <= last_s_index:
s = self.strings[index]
if s:
yield s
if index < last_s_index:
yield self.i_indexes[index]
index += 1
Loading