Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit f7e6e59

Browse files
authored
Substantial refactor more along the lines I’d like to ship (#1)
* WIP: Refactor node handling * WIP: clean up the HTML parsing stuff. * Move clsx -> separate classnames.py * Clean up format_interpolation() and convert() * Take a hard dependency on MarkupSafe * Getting closer with clean impl. * This is starting to feel coherent.
1 parent aee542b commit f7e6e59

17 files changed

+1724
-1364
lines changed

html_tstring/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from markupsafe import Markup, escape
2+
3+
from .nodes import Comment, DocumentType, Element, Fragment, Text
4+
from .processor import html
5+
6+
# We consider `Markup` and `escape` to be part of this module's public API
7+
8+
__all__ = [
9+
"Comment",
10+
"DocumentType",
11+
"Element",
12+
"escape",
13+
"Fragment",
14+
"html",
15+
"Markup",
16+
"Text",
17+
]

html_tstring/classnames.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
def classnames(*args: object) -> str:
2+
"""
3+
Construct a space-separated class string from various inputs.
4+
5+
Accepts strings, lists/tuples of strings, and dicts mapping class names to
6+
boolean values. Ignores None and False values.
7+
8+
Examples:
9+
classnames("btn", "btn-primary") -> "btn btn-primary"
10+
classnames("btn", {"btn-primary": True, "disabled": False}) -> "btn btn-primary"
11+
classnames(["btn", "btn-primary"], {"disabled": True}) -> "btn btn-primary disabled"
12+
classnames("btn", None, False, "active") -> "btn active"
13+
14+
Args:
15+
*args: Variable length argument list containing strings, lists/tuples,
16+
or dicts.
17+
18+
Returns:
19+
A single string with class names separated by spaces.
20+
"""
21+
classes: list[str] = []
22+
# Use a queue to process arguments iteratively, preserving order.
23+
queue = list(args)
24+
25+
while queue:
26+
arg = queue.pop(0)
27+
28+
if not arg: # Handles None, False, empty strings/lists/dicts
29+
continue
30+
31+
if isinstance(arg, str):
32+
classes.append(arg)
33+
elif isinstance(arg, dict):
34+
for key, value in arg.items():
35+
if value:
36+
classes.append(key)
37+
elif isinstance(arg, (list, tuple)):
38+
# Add items to the front of the queue to process them next, in order.
39+
queue[0:0] = arg
40+
elif isinstance(arg, bool):
41+
pass # Explicitly ignore booleans not in a dict
42+
else:
43+
raise ValueError(f"Invalid class argument type: {type(arg).__name__}")
44+
45+
# Filter out empty strings and join the result.
46+
return " ".join(stripped for c in classes if (stripped := c.strip()))

html_tstring/classnames_test.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import pytest
2+
3+
from .classnames import classnames
4+
5+
6+
def test_classnames_empty():
7+
assert classnames() == ""
8+
9+
10+
def test_classnames_strings():
11+
assert classnames("btn", "btn-primary") == "btn btn-primary"
12+
13+
14+
def test_classnames_strings_strip():
15+
assert classnames(" btn ", " btn-primary ") == "btn btn-primary"
16+
17+
18+
def test_cslx_empty_strings():
19+
assert classnames("", "btn", "", "btn-primary", "") == "btn btn-primary"
20+
21+
22+
def test_classnames_lists_and_tuples():
23+
assert (
24+
classnames(["btn", "btn-primary"], ("active", "disabled"))
25+
== "btn btn-primary active disabled"
26+
)
27+
28+
29+
def test_classnames_dicts():
30+
assert (
31+
classnames(
32+
"btn",
33+
{"btn-primary": True, "disabled": False, "active": True, "shown": "yes"},
34+
)
35+
== "btn btn-primary active shown"
36+
)
37+
38+
39+
def test_classnames_mixed_inputs():
40+
assert (
41+
classnames(
42+
"btn",
43+
["btn-primary", "active"],
44+
{"disabled": True, "hidden": False},
45+
("extra",),
46+
)
47+
== "btn btn-primary active disabled extra"
48+
)
49+
50+
51+
def test_classnames_ignores_none_and_false():
52+
assert (
53+
classnames("btn", None, False, "active", {"hidden": None, "visible": True})
54+
== "btn active visible"
55+
)
56+
57+
58+
def test_classnames_raises_type_error_on_invalid_input():
59+
with pytest.raises(ValueError):
60+
classnames(123)
61+
62+
with pytest.raises(ValueError):
63+
classnames(["btn", 456])
64+
65+
66+
def test_classnames_kitchen_sink():
67+
assert (
68+
classnames(
69+
"foo",
70+
[1 and "bar", {"baz": False, "bat": None}, ["hello", ["world"]]],
71+
"cya",
72+
)
73+
== "foo bar hello world cya"
74+
)

html_tstring/element.py

Lines changed: 0 additions & 119 deletions
This file was deleted.

0 commit comments

Comments
 (0)