Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
ab74226
Add helper method to parallel Template.__iter__.
ianjosephwilson Feb 10, 2026
0c76fc7
Move html dunder protocol into module and add tests.
ianjosephwilson Feb 10, 2026
da0337c
Split out component kwargs resolution for testing/reusability.
ianjosephwilson Feb 10, 2026
a661404
Rename html to to_node and introduce to_html for string serialization.
ianjosephwilson Feb 10, 2026
a412e6f
Conditionally allow Markup for tags with non-normal text.
ianjosephwilson Feb 10, 2026
8204698
First draft using transformed templates.
ianjosephwilson Feb 10, 2026
4f9fd5c
Move html spec info into utility module.
ianjosephwilson Feb 14, 2026
9fcd2b7
Add shorthand for pulling interpolations by index back into a template.
ianjosephwilson Feb 15, 2026
a5a5d06
Move node builder into nodes.
ianjosephwilson Feb 15, 2026
fd18de6
Remove extra arg to escaping.
ianjosephwilson Feb 15, 2026
678755b
Unescaped entities need to be re-escaped.
ianjosephwilson Feb 15, 2026
c233581
Coerce tests into two processor strategies.
ianjosephwilson Feb 15, 2026
2c7f718
Parsed entities should be re-escaped by default
ianjosephwilson Feb 15, 2026
df2854f
Formatting.
ianjosephwilson Feb 15, 2026
6036697
Use runtime parent tag when component has no predefined parent tag.
ianjosephwilson Feb 15, 2026
96f4654
Split assertions and correct the expected output.
ianjosephwilson Feb 16, 2026
027ddd5
Add callback as explicit opt-in formatter.
ianjosephwilson Feb 16, 2026
bcd5b74
Merge extra transform processor tests into main processor tests file.
ianjosephwilson Feb 16, 2026
55af193
Remove tests that no longer apply.
ianjosephwilson Feb 16, 2026
79955e5
Group very special case tests together with class.
ianjosephwilson Feb 16, 2026
32fd5ca
Do not call str() prematurely to allow __html__ through.
ianjosephwilson Feb 17, 2026
2f9f88f
Explicitly allow __html__ from exact interpolations in non-normal texts.
ianjosephwilson Feb 17, 2026
e8be5cf
Safe is not needed in test anymore.
ianjosephwilson Feb 17, 2026
4fcfb0b
Use protocol
ianjosephwilson Feb 17, 2026
280c432
Draft direct str processor.
ianjosephwilson Feb 17, 2026
0179537
Skip attrs resolution if there are no attrs.
ianjosephwilson Feb 17, 2026
68c7d73
Skip attrs resolution if there are no attrs.
ianjosephwilson Feb 17, 2026
4169214
Lint and format.
ianjosephwilson Feb 18, 2026
8f12935
Partial redraft of the doc tests.
ianjosephwilson Feb 18, 2026
7d84ce3
Move nodes and nodes processor into submodule.
ianjosephwilson Feb 18, 2026
486ecb6
Fill out missing annotations.
ianjosephwilson Feb 18, 2026
ed27704
Move sentinel into module for now.
ianjosephwilson Feb 18, 2026
7eb34c0
Don't cache this until later if needed at all.
ianjosephwilson Feb 18, 2026
9175ce8
Format.
ianjosephwilson Feb 18, 2026
383da8e
Fix typing in process ctx and make resolution more explicit.
ianjosephwilson Feb 18, 2026
3b8af32
Remove system hook because it can be used this way anymore.
ianjosephwilson Feb 21, 2026
e3a8915
Properly handle None in text resolved without recursion.
ianjosephwilson Feb 21, 2026
9b7969c
Regroup element tests by content type.
ianjosephwilson Feb 21, 2026
326c6cb
Correctly handle None for resolve without recursion texts in to_node.
ianjosephwilson Feb 21, 2026
b1a2a42
Move placeholder collision test from processor to parser tests.
ianjosephwilson Feb 21, 2026
2bda54c
Format.
ianjosephwilson Feb 21, 2026
b65eb07
Start to unify testing between str processor and node processor.
ianjosephwilson Feb 21, 2026
8086295
Replace duplicate tests with basic integration test.
ianjosephwilson Feb 21, 2026
62b60cd
Drop stream naming in favor of process, fixup return types, only act …
ianjosephwilson Feb 22, 2026
6a309ba
Context changes inside method so don't alter it before.
ianjosephwilson Feb 22, 2026
3603175
Realign node processor with str processor.
ianjosephwilson Feb 22, 2026
49af615
Actually utilize TemplateRef.
ianjosephwilson Feb 22, 2026
03222e7
Add test for entity re-escaping in literals in normal test.
ianjosephwilson Feb 22, 2026
438e303
Fix import and type annotation.
ianjosephwilson Feb 22, 2026
c1cdcd7
Apply format_spec to raw text and escapable raw text interpolations.
ianjosephwilson Feb 22, 2026
99d2301
Test format_spec and conversion for escapable raw text and raw text.
ianjosephwilson Feb 22, 2026
422fe8f
We did this.
ianjosephwilson Feb 22, 2026
1324c6d
Restore processor cache vs nocache test.
ianjosephwilson Feb 22, 2026
3bd76e2
Delegate caching/parsing to an injected service.
ianjosephwilson Feb 22, 2026
c3440fc
Update cache test to use new injected service.
ianjosephwilson Feb 22, 2026
ba58701
Format
ianjosephwilson Feb 22, 2026
90c6040
Fix import.
ianjosephwilson Feb 22, 2026
8cb5161
Add instance check for clarity and ... type check.
ianjosephwilson Feb 22, 2026
8e78111
Add casts to trigger type checking of important value transitions, ad…
ianjosephwilson Feb 22, 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
115 changes: 23 additions & 92 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ T-strings work just like f-strings but use a `t` prefix and
instead of strings.

Once you have a `Template`, you can call this package's `html()` function to
convert it into a tree of `Node` objects that represent your HTML structure.
From there, you can render it to a string, manipulate it programmatically, or
compose it with other templates for maximum flexibility.
convert it into a str.

### Getting Started

Expand All @@ -53,7 +51,6 @@ Import the `html` function and start creating templates:
```python
from tdom import html
greeting = html(t"<h1>Hello, World!</h1>")
print(type(greeting)) # <class 'tdom.nodes.Element'>
print(greeting) # <h1>Hello, World!</h1>
```

Expand Down Expand Up @@ -145,7 +142,7 @@ 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>'
assert button == '<button class="btn btn-primary">Click me</button>'
```

#### The `style` Attribute
Expand All @@ -166,7 +163,7 @@ 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>'
assert para == '<p style="color: red; font-weight: bold">Important text</p>'
```

#### The `data` and `aria` Attributes
Expand Down Expand Up @@ -312,18 +309,6 @@ page = html(t"<div>{content}</div>")
# <div><h1>My Site</h1></div>
```

In the example above, `content` is a `Template` object that gets correctly
parsed and embedded within the outer template. You can also explicitly call
`html()` on nested templates if you prefer:

```python
content = html(t"<h1>My Site</h1>")
page = html(t"<div>{content}</div>")
# <div><h1>My Site</h1></div>
```

The result is the same either way.

#### Component Functions

You can create reusable component functions that generate templates with dynamic
Expand All @@ -333,10 +318,11 @@ The basic form of all component functions is:

```python
from typing import Any, Iterable
from tdom import Node, html
from tdom import html
from string.templatelib import Template

def MyComponent(children: Iterable[Node], **attrs: Any) -> Node:
return html(t"<div {attrs}>Cool: {children}</div>")
def MyComponent(children: Template, **attrs: Any) -> Template:
return t"<div {attrs}>Cool: {children}</div>"
```

To _invoke_ your component within an HTML template, use the special
Expand All @@ -352,10 +338,11 @@ type hints for better editor support:

```python
from typing import Any
from tdom import Node, html
from tdom import html
from string.templatelib import Template

def Link(*, href: str, text: str, data_value: int, **attrs: Any) -> Node:
return html(t'<a href="{href}" {attrs}>{text}: {data_value}</a>')
def Link(*, href: str, text: str, data_value: int, **attrs: Any) -> Template:
return t'<a href="{href}" {attrs}>{text}: {data_value}</a>'

result = html(t'<{Link} href="https://example.com" text="Example" data-value={42} target="_blank" />')
# <a href="https://example.com" target="_blank">Example: 42</a>
Expand All @@ -364,12 +351,7 @@ result = html(t'<{Link} href="https://example.com" text="Example" data-value={42
Note that attributes with hyphens (like `data-value`) are converted to
underscores (`data_value`) in the function signature.

Component functions build children and can return _any_ type of value; the
returned value will be treated exactly as if it were placed directly in a child
position in the template.

Among other things, this means you can return a `Template` directly from a
component function:
Component functions build Templates that conceptually will replace the component tags with the new template.

<!-- invisible-code-block: python
from string.templatelib import Template
Expand All @@ -380,23 +362,7 @@ def Greeting(name: str) -> Template:
return t"<h1>Hello, {name}!</h1>"

result = html(t"<{Greeting} name='Alice' />")
assert str(result) == "<h1>Hello, Alice!</h1>"
```

You may also return an iterable:

<!-- invisible-code-block: python
from string.templatelib import Template
-->

```python
from typing import Iterable

def Items() -> Iterable[Template]:
return [t"<li>first</li>", t"<li>second</li>"]

result = html(t"<ul><{Items} /></ul>")
assert str(result) == "<ul><li>first</li><li>second</li></ul>"
assert result == "<h1>Hello, Alice!</h1>"
```

#### Class-based components
Expand All @@ -416,18 +382,18 @@ from tdom import Node, html

@dataclass
class Card:
children: Iterable[Node]
children: Template
title: str
subtitle: str | None = None

def __call__(self) -> Node:
return html(t"""
def __call__(self) -> Template:
return t"""
<div class='card'>
<h2>{self.title}</h2>
{self.subtitle and t'<h3>{self.subtitle}</h3>'}
<div class="content">{self.children}</div>
</div>
""")
"""

result = html(t"<{Card} title='My Card' subtitle='A subtitle'><p>Card content</p></{Card}>")
# <div class='card'>
Expand All @@ -442,7 +408,7 @@ class, making it easier to manage complex components.

As a note, `children` are optional in component signatures. If a component
requests children, it will receive them if provided. If no children are
provided, the value of children is an empty tuple. If the component does _not_
provided, the value of children is an empty Template, ie. `t""`. If the component does _not_
ask for children, but they are provided, then they are silently ignored.

#### SVG Support
Expand Down Expand Up @@ -470,12 +436,12 @@ All the same interpolation, attribute handling, and component features work with
SVG elements:

```python
def Icon(*, size: int = 24, color: str = "currentColor") -> Node:
return html(t"""
def Icon(*, size: int = 24, color: str = "currentColor") -> Template:
return t"""
<svg width="{size}" height="{size}" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="{color}" stroke-width="2"/>
</svg>
""")
"""

result = html(t'<{Icon} size={48} color="blue" />')
assert 'width="48"' in str(result)
Expand All @@ -497,9 +463,9 @@ options:
```python
theme = {"primary": "blue", "spacing": "10px"}

def Button(text: str) -> Node:
def Button(text: str) -> Template:
# Button has access to theme from enclosing scope
return html(t'<button style="color: {theme["primary"]}; margin: {theme["spacing"]}">{text}</button>')
return t'<button style="color: {theme["primary"]}; margin: {theme["spacing"]}">{text}</button>'

result = html(t'<{Button} text="Click me" />')
assert 'color: blue' in str(result)
Expand All @@ -518,41 +484,6 @@ This explicit approach makes it clear where data comes from and avoids the

### The `tdom` Module

#### Working with `Node` Objects

While `html()` is the primary way to create nodes, you can also construct them
directly for programmatic HTML generation:

```python
from tdom import Element, Text, Fragment, Comment, DocumentType

# Create elements directly
div = Element("div", attrs={"class": "container"}, children=[
Text("Hello, "),
Element("strong", children=[Text("World")]),
])
assert str(div) == '<div class="container">Hello, <strong>World</strong></div>'

# Create fragments to group multiple nodes
fragment = Fragment(children=[
Element("h1", children=[Text("Title")]),
Element("p", children=[Text("Paragraph")]),
])
assert str(fragment) == "<h1>Title</h1><p>Paragraph</p>"

# Add comments
page = Element("body", children=[
Comment("Navigation section"),
Element("nav", children=[Text("Nav content")]),
])
assert str(page) == "<body><!--Navigation section--><nav>Nav content</nav></body>"
```

All nodes implement the `__html__()` protocol, which means they can be used
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.

#### Utilities

The `tdom` package includes several utility functions for working with
Expand Down
47 changes: 21 additions & 26 deletions docs/usage/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ function with normal Python arguments and return values.
## Simple Heading

Here is a component callable &mdash; a `Heading` function &mdash; which returns
a `Node`:
a `Template`:

<!-- invisible-code-block: python
from string.templatelib import Template
from tdom import html, Node
from tdom import html
from typing import Callable, Iterable
-->

Expand All @@ -39,7 +39,7 @@ def Heading() -> Template:


result = html(t"<{Heading} />")
assert str(result) == '<h1>My Title</h1>'
assert result == '<h1>My Title</h1>'
```

## Simple Props
Expand All @@ -54,7 +54,7 @@ def Heading(title: str) -> Template:


result = html(t'<{Heading} title="My Title"></{Heading}>')
assert str(result) == '<h1>My Title</h1>'
assert result == '<h1>My Title</h1>'
```

## Children As Props
Expand All @@ -63,32 +63,29 @@ If your template has children inside the component element, your component will
receive them as a keyword argument:

```python
def Heading(children: Iterable[Node], title: str) -> Node:
return html(t"<h1>{title}</h1><div>{children}</div>")
def Heading(children: Template, title: str) -> Template:
return t"<h1>{title}</h1><div>{children}</div>"


result = html(t'<{Heading} title="My Title">Child</{Heading}>')
assert str(result) == '<h1>My Title</h1><div>Child</div>'
assert result == '<h1>My Title</h1><div>Child</div>'
```

Note how the component closes with `</{Heading}>` when it contains nested
children, as opposed to the self-closing form in the first example. If no
children are provided, the value of children is an empty tuple.

Note also that components functions can return `Node` or `Template` values as
they wish. Iterables of nodes and templates are also supported.
children are provided, the value of children is an empty `Template`, ie. `t""`.

The component does not have to list a `children` keyword argument. If it is
omitted from the function parameters and passed in by the usage, it is silently
ignored:

```python
def Heading(title: str) -> Node:
return html(t"<h1>{title}</h1><div>Ignore the children.</div>")
def Heading(title: str) -> Template:
return t"<h1>{title}</h1><div>Ignore the children.</div>"


result = html(t'<{Heading} title="My Title">Child</{Heading}>')
assert str(result) == '<h1>My Title</h1><div>Ignore the children.</div>'
assert result == '<h1>My Title</h1><div>Ignore the children.</div>'
```

## Optional Props
Expand All @@ -102,7 +99,7 @@ def Heading(title: str = "My Title") -> Template:


result = html(t"<{Heading} />")
assert str(result) == '<h1>My Title</h1>'
assert result == '<h1>My Title</h1>'
```

## Passsing Another Component as a Prop
Expand All @@ -121,7 +118,7 @@ def Body(heading: Callable) -> Template:


result = html(t"<{Body} heading={DefaultHeading} />")
assert str(result) == '<body><h1>Default Heading</h1></body>'
assert result == '<body><h1>Default Heading</h1></body>'
```

## Default Component for Prop
Expand All @@ -139,11 +136,11 @@ def OtherHeading() -> Template:


def Body(heading: Callable) -> Template:
return html(t"<body><{heading} /></body>")
return t"<body><{heading} /></body>"


result = html(t"<{Body} heading={OtherHeading}></{Body}>")
assert str(result) == '<body><h1>Other Heading</h1></body>'
assert result == '<body><h1>Other Heading</h1></body>'
```

## Conditional Default
Expand All @@ -165,7 +162,7 @@ def Body(heading: Callable | None = None) -> Template:


result = html(t"<{Body} heading={OtherHeading}></{Body}>")
assert str(result) == '<body><h1>Other Heading</h1></body>'
assert result == '<body><h1>Other Heading</h1></body>'
```

## Generators as Components
Expand All @@ -175,13 +172,11 @@ have a todo list. There might be a lot of todos, so you want to generate them in
a memory-efficient way:

```python
def Todos() -> Iterable[Template]:
for todo in ["first", "second", "third"]:
yield t"<li>{todo}</li>"

def Todos() -> Template:
return t'<ul>{(t"<li>{todo}</li>" for todo in ["first", "second", "third"])}</ul>'

result = html(t"<ul><{Todos} /></ul>")
assert str(result) == '<ul><li>first</li><li>second</li><li>third</li></ul>'
result = html(t"<{Todos} />")
assert result == '<ul><li>first</li><li>second</li><li>third</li></ul>'
```

## Nested Components
Expand All @@ -200,5 +195,5 @@ def TodoList(labels: Iterable[str]) -> Template:
title = "My Todos"
labels = ["first", "second", "third"]
result = html(t"<h1>{title}</h1><{TodoList} labels={labels} />")
assert str(result) == '<h1>My Todos</h1><ul><li>first</li><li>second</li><li>third</li></ul>'
assert result == '<h1>My Todos</h1><ul><li>first</li><li>second</li><li>third</li></ul>'
```
4 changes: 2 additions & 2 deletions docs/usage/looping.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ then use that `Node` result in the next template:
```python
message = "Hello"
names = ["World", "Universe"]
items = [html(t"<li>{label}</li>") for label in names]
items = [t"<li>{label}</li>" for label in names]
result = html(t"<ul title={message}>{items}</ul>")
assert str(result) == '<ul title="Hello"><li>World</li><li>Universe</li></ul>'
assert result == '<ul title="Hello"><li>World</li><li>Universe</li></ul>'
```
Loading