Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.
Merged
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
17 changes: 17 additions & 0 deletions html_tstring/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from markupsafe import Markup, escape

from .nodes import Comment, DocumentType, Element, Fragment, Text
from .processor import html

# We consider `Markup` and `escape` to be part of this module's public API

__all__ = [
"Comment",
"DocumentType",
"Element",
"escape",
"Fragment",
"html",
"Markup",
"Text",
]
46 changes: 46 additions & 0 deletions html_tstring/classnames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
def classnames(*args: object) -> str:
"""
Construct a space-separated class string from various inputs.

Accepts strings, lists/tuples of strings, and dicts mapping class names to
boolean values. Ignores None and False values.

Examples:
classnames("btn", "btn-primary") -> "btn btn-primary"
classnames("btn", {"btn-primary": True, "disabled": False}) -> "btn btn-primary"
classnames(["btn", "btn-primary"], {"disabled": True}) -> "btn btn-primary disabled"
classnames("btn", None, False, "active") -> "btn active"

Args:
*args: Variable length argument list containing strings, lists/tuples,
or dicts.

Returns:
A single string with class names separated by spaces.
"""
classes: list[str] = []
# Use a queue to process arguments iteratively, preserving order.
queue = list(args)

while queue:
arg = queue.pop(0)

if not arg: # Handles None, False, empty strings/lists/dicts
continue

if isinstance(arg, str):
classes.append(arg)
elif isinstance(arg, dict):
for key, value in arg.items():
if value:
classes.append(key)
elif isinstance(arg, (list, tuple)):
# Add items to the front of the queue to process them next, in order.
queue[0:0] = arg
elif isinstance(arg, bool):
pass # Explicitly ignore booleans not in a dict
else:
raise ValueError(f"Invalid class argument type: {type(arg).__name__}")

# Filter out empty strings and join the result.
return " ".join(stripped for c in classes if (stripped := c.strip()))
74 changes: 74 additions & 0 deletions html_tstring/classnames_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import pytest

from .classnames import classnames


def test_classnames_empty():
assert classnames() == ""


def test_classnames_strings():
assert classnames("btn", "btn-primary") == "btn btn-primary"


def test_classnames_strings_strip():
assert classnames(" btn ", " btn-primary ") == "btn btn-primary"


def test_cslx_empty_strings():
assert classnames("", "btn", "", "btn-primary", "") == "btn btn-primary"


def test_classnames_lists_and_tuples():
assert (
classnames(["btn", "btn-primary"], ("active", "disabled"))
== "btn btn-primary active disabled"
)


def test_classnames_dicts():
assert (
classnames(
"btn",
{"btn-primary": True, "disabled": False, "active": True, "shown": "yes"},
)
== "btn btn-primary active shown"
)


def test_classnames_mixed_inputs():
assert (
classnames(
"btn",
["btn-primary", "active"],
{"disabled": True, "hidden": False},
("extra",),
)
== "btn btn-primary active disabled extra"
)


def test_classnames_ignores_none_and_false():
assert (
classnames("btn", None, False, "active", {"hidden": None, "visible": True})
== "btn active visible"
)


def test_classnames_raises_type_error_on_invalid_input():
with pytest.raises(ValueError):
classnames(123)

with pytest.raises(ValueError):
classnames(["btn", 456])


def test_classnames_kitchen_sink():
assert (
classnames(
"foo",
[1 and "bar", {"baz": False, "bat": None}, ["hello", ["world"]]],
"cya",
)
== "foo bar hello world cya"
)
119 changes: 0 additions & 119 deletions html_tstring/element.py

This file was deleted.

Loading
Loading