Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b2a0ea0
Wrap up low level placeholder handling into PlaceholderConfig helper …
ianjosephwilson Dec 18, 2025
ca44b4a
Draft version of special class handling with merging and distinct seq…
ianjosephwilson Dec 21, 2025
b3ca458
Use True instead of None since we only init from literal.
ianjosephwilson Dec 21, 2025
decae08
Add style equivalent to class merging.
ianjosephwilson Dec 21, 2025
3acbb4b
Remove old style processing code.
ianjosephwilson Dec 21, 2025
f3290a2
Add mixed class types back in but without sequence nesting.
ianjosephwilson Dec 21, 2025
b83ed6d
Allow attributes created via aria dict to clear literal attributes.
ianjosephwilson Dec 22, 2025
8616767
Allow attributes created via data dict to clear literal attributes.
ianjosephwilson Dec 22, 2025
662fe24
Move special class and style handling state and logic into helper cla…
ianjosephwilson Dec 22, 2025
119fffb
Expand style testing.
ianjosephwilson Dec 23, 2025
e0b8c8e
Sp.
ianjosephwilson Dec 26, 2025
bc97947
Try to uniform design with prior processor.
ianjosephwilson Dec 26, 2025
87bea0e
Make things *right.
ianjosephwilson Dec 26, 2025
93e8d40
Rename to expander to accommodate allocator, pull up into main resolu…
ianjosephwilson Dec 26, 2025
50ef974
Re-format.
ianjosephwilson Dec 26, 2025
df2708e
Remove excess docstrings.
ianjosephwilson Dec 26, 2025
c495ab8
Adjust docs, drop classnames helper.
ianjosephwilson Dec 26, 2025
61ead88
Ignore None to easily support passing through a kwarg with a None def…
ianjosephwilson Jan 2, 2026
0028f2d
Allow special attributes to be None to allow pass-through from compon…
ianjosephwilson Jan 5, 2026
224e120
Try to add all forms to class attr smoketest.
ianjosephwilson Jan 9, 2026
9ea0512
Restrict each class attr to either toggle or add list for now.
ianjosephwilson Jan 9, 2026
45be164
Drop support for booleans in interpolated class attributes.
ianjosephwilson Jan 9, 2026
0862d14
Add space separated classes to smoketest.
ianjosephwilson Jan 9, 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
70 changes: 24 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,23 +130,29 @@ button = html(t'<button class="{classes}">Click me</button>')
# <button class="btn btn-primary active">Click me</button>
```

For flexibility, you can also provide a list of strings, dictionaries, or a mix
of both:
The `class` attribute can also be a dictionary to toggle classes on or off:

```python
classes = ["btn", "btn-primary", {"active": True}, None, False and "disabled"]
button = html(t'<button class="{classes}">Click me</button>')
classes = {"active": True, "btn-primary": True}
button = html(t'<button class={classes}>Click me</button>')
# <button class="btn btn-primary active">Click me</button>
```

See the
[`classnames()`](https://github.com/t-strings/tdom/blob/main/tdom/classnames_test.py)
helper function for more information on how class names are combined.
The `class` attribute can be specified more than once. The values are merged
from left to right. A common use case would be to update and/or extend default
classes:

```python
classes = {"btn-primary": True, "btn-secondary": False}
button = html(t'<button class="btn btn-secondary" class={classes}>Click me</button>')
assert str(button) == '<button class="btn btn-primary">Click me</button>'
```

#### The `style` Attribute

In addition to strings, you can also provide a dictionary of CSS properties and
values for the `style` attribute:
The `style` attribute has special handling to make it easy to combine multiple
styles from different sources. The simplest way is to provide a dictionary of
CSS properties and values for the `style` attribute:

```python
# Style attributes from dictionaries
Expand All @@ -155,6 +161,14 @@ styled = html(t"<p style={styles}>Important text</p>")
# <p style="color: red; font-weight: bold; margin: 10px">Important text</p>
```

Style attributes can also be merged to extend a base style:

```python
add_styles = {"font-weight": "bold"}
para = html(t'<p style="color: red" style={add_styles}>Important text</p>')
assert str(para) == '<p style="color: red; font-weight: bold">Important text</p>'
```

#### The `data` and `aria` Attributes

The `data` and `aria` attributes also have special handling to convert
Expand Down Expand Up @@ -195,7 +209,7 @@ Special attributes likes `class` behave as expected when combined with
spreading:

```python
classes = ["btn", {"active": True}]
classes = {"btn": True, "active": True}
attrs = {"class": classes, "id": "act_now", "data": {"wow": "such-attr"}}
button = html(t'<button {attrs}>Click me</button>')
# <button class="btn active" id="act_now" data-wow="such-attr">Click me</button>
Expand Down Expand Up @@ -539,42 +553,6 @@ anywhere that expects an object with HTML representation. Converting a node to a
string (via `str()` or `print()`) automatically renders it as HTML with proper
escaping.

#### The `classnames()` Helper

The `classnames()` function provides a flexible way to build class name strings
from various input types. It's particularly useful when you need to
conditionally include classes:

```python
from tdom import classnames

# Combine strings
assert classnames("btn", "btn-primary") == "btn btn-primary"

# Use dictionaries for conditional classes
is_active = True
is_disabled = False
assert classnames("btn", {
"btn-active": is_active,
"btn-disabled": is_disabled
}) == "btn btn-active"

# Mix lists, dicts, and strings
assert classnames(
"btn",
["btn-large", "rounded"],
{"btn-primary": True, "btn-secondary": False},
None, # Ignored
False # Ignored
) == "btn btn-large rounded btn-primary"

# Nested lists are flattened
assert classnames(["btn", ["btn-primary", ["active"]]]) == "btn btn-primary active"
```

This function is automatically used when processing `class` attributes in
templates, so you can pass any of these input types directly in your t-strings.

#### Utilities

The `tdom` package includes several utility functions for working with
Expand Down
2 changes: 0 additions & 2 deletions tdom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from markupsafe import Markup, escape

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

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

__all__ = [
"classnames",
"Comment",
"DocumentType",
"Element",
Expand Down
50 changes: 0 additions & 50 deletions tdom/classnames.py

This file was deleted.

78 changes: 0 additions & 78 deletions tdom/classnames_test.py

This file was deleted.

79 changes: 44 additions & 35 deletions tdom/placeholders.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,72 @@
from dataclasses import dataclass, field
import random
import re
import string

from .template_utils import TemplateRef


_PLACEHOLDER_PREFIX = f"t🐍{''.join(random.choices(string.ascii_lowercase, k=2))}-"
_PLACEHOLDER_SUFFIX = f"-{''.join(random.choices(string.ascii_lowercase, k=2))}🐍t"
_PLACEHOLDER_PATTERN = re.compile(
re.escape(_PLACEHOLDER_PREFIX) + r"(\d+)" + re.escape(_PLACEHOLDER_SUFFIX)
)
def make_placeholder_config() -> PlaceholderConfig:
prefix = f"t🐍{''.join(random.choices(string.ascii_lowercase, k=2))}-"
suffix = f"-{''.join(random.choices(string.ascii_lowercase, k=2))}🐍t"
return PlaceholderConfig(
prefix=prefix,
suffix=suffix,
pattern=re.compile(re.escape(prefix) + r"(\d+)" + re.escape(suffix)),
)


def make_placeholder(i: int) -> str:
"""Generate a placeholder for the i-th interpolation."""
return f"{_PLACEHOLDER_PREFIX}{i}{_PLACEHOLDER_SUFFIX}"
@dataclass(frozen=True)
class PlaceholderConfig:
"""String operations for working with a placeholder pattern."""

prefix: str
suffix: str
pattern: re.Pattern

def match_placeholders(s: str) -> list[re.Match[str]]:
"""Find all placeholders in a string."""
return list(_PLACEHOLDER_PATTERN.finditer(s))
def make_placeholder(self, i: int) -> str:
"""Generate a placeholder for the i-th interpolation."""
return f"{self.prefix}{i}{self.suffix}"

def match_placeholders(self, s: str) -> list[re.Match[str]]:
"""Find all placeholders in a string."""
return list(self.pattern.finditer(s))

def find_placeholders(s: str) -> TemplateRef:
"""
Find all placeholders in a string and return a TemplateRef.
def find_placeholders(self, s: str) -> TemplateRef:
"""
Find all placeholders in a string and return a TemplateRef.

If no placeholders are found, returns a static TemplateRef.
"""
matches = match_placeholders(s)
if not matches:
return TemplateRef.literal(s)
If no placeholders are found, returns a static TemplateRef.
"""
matches = self.match_placeholders(s)
if not matches:
return TemplateRef.literal(s)

strings: list[str] = []
i_indexes: list[int] = []
last_index = 0
for match in matches:
start, end = match.span()
strings.append(s[last_index:start])
i_indexes.append(int(match[1]))
last_index = end
strings.append(s[last_index:])
strings: list[str] = []
i_indexes: list[int] = []
last_index = 0
for match in matches:
start, end = match.span()
strings.append(s[last_index:start])
i_indexes.append(int(match[1]))
last_index = end
strings.append(s[last_index:])

return TemplateRef(tuple(strings), tuple(i_indexes))
return TemplateRef(tuple(strings), tuple(i_indexes))


@dataclass
class PlaceholderState:
known: set[int]
known: set[int] = field(default_factory=set)
config: PlaceholderConfig = field(default_factory=make_placeholder_config)
"""Collection of currently 'known and active' placeholder indexes."""

def __init__(self):
self.known = set()

@property
def is_empty(self) -> bool:
return len(self.known) == 0

def add_placeholder(self, index: int) -> str:
placeholder = make_placeholder(index)
placeholder = self.config.make_placeholder(index)
self.known.add(index)
return placeholder

Expand All @@ -69,7 +78,7 @@ def remove_placeholders(self, text: str) -> TemplateRef:

If no placeholders are found, returns a static PlaceholderRef.
"""
pt = find_placeholders(text)
pt = self.config.find_placeholders(text)
for index in pt.i_indexes:
if index not in self.known:
raise ValueError(f"Unknown placeholder index {index} found in text.")
Expand Down
Loading