Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c64202f
Conditionally allow Markup for tags with non-normal text.
ianjosephwilson Feb 10, 2026
4c00e3f
Add template processor.
ianjosephwilson Mar 28, 2026
1ebf11c
Start updating docs.
ianjosephwilson Mar 29, 2026
6701911
Handle svg during processing.
ianjosephwilson Mar 30, 2026
01a77c9
Drop nodes.
ianjosephwilson Mar 29, 2026
4af611f
Experimental system context implementation.
ianjosephwilson Feb 23, 2026
88892ab
Add math and foreignobject support.
ianjosephwilson Mar 29, 2026
8eb23da
Remove old svg tests. Unskip mathml test.
ianjosephwilson Apr 3, 2026
3ef9773
Unfactor base classes after removing Node processor.
ianjosephwilson Apr 3, 2026
42f902e
Remove optimization.
ianjosephwilson Apr 7, 2026
452fc24
Restrict internal apis to only using list.append to produce the outpu…
ianjosephwilson Apr 7, 2026
c420158
Remove chunking optimization completely.
ianjosephwilson Apr 7, 2026
05e0182
Explicitly return str.
ianjosephwilson Apr 7, 2026
204aded
Start trying to fix doc strings.
ianjosephwilson Apr 7, 2026
0675aa6
Chunk t-element processing into a method to improve readability.
ianjosephwilson Apr 7, 2026
ce69276
Use dedicated variable for clarity.
ianjosephwilson Apr 7, 2026
5bd6878
Remove unused type.
ianjosephwilson Apr 8, 2026
85b9c76
Push normal text processing around to matchup with raw and escapable …
ianjosephwilson Apr 8, 2026
6f9cb4c
Remove suffix from types.
ianjosephwilson Apr 9, 2026
dacd029
Formatting.
ianjosephwilson Apr 9, 2026
7e51877
Be more explicit about HasHTMLDunder since it might not be Markup.
ianjosephwilson Apr 10, 2026
e68e05a
Make sure Markup and a custom HasHTMLDunder implementation are consis…
ianjosephwilson Apr 10, 2026
d759c17
Formatting...
ianjosephwilson Apr 10, 2026
461ba3c
Commit process_tnode to dispatching.
ianjosephwilson Apr 10, 2026
889c0b7
Breakup conditional and add explanation.
ianjosephwilson Apr 10, 2026
582d64b
Add back bool support and remove some unecessary optimizations to str…
ianjosephwilson Apr 10, 2026
eacff4f
Formatting.
ianjosephwilson Apr 10, 2026
f92d5ca
Drop system support, extend children kwarg coverage.
ianjosephwilson Apr 10, 2026
b0b5e18
Add bool tests.
ianjosephwilson Apr 11, 2026
b753510
Remove now unused class.
ianjosephwilson Apr 11, 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
116 changes: 34 additions & 82 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.
render it to a string.

### 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 @@ -332,11 +329,13 @@ content and attributes. Use these like custom HTML elements in your templates.
The basic form of all component functions is:

```python
from string.templatelib import Template

from typing import Any, Iterable
from tdom import Node, html
from tdom import html

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 @@ -351,11 +350,13 @@ Because attributes are passed as keyword arguments, you can explicitly provide
type hints for better editor support:

```python
from string.templatelib import Template

from typing import Any
from tdom import Node, html
from tdom import html

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 @@ -380,23 +381,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 @@ -410,24 +395,25 @@ One particularly useful pattern is to build class-based components with
dataclasses:

```python
from string.templatelib import Template
from dataclasses import dataclass, field
from typing import Any, Iterable
from tdom import Node, html
from tdom import 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 @@ -452,7 +438,8 @@ syntax as HTML. You can create inline SVG graphics by simply including SVG tags
in your templates:

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

```python
Expand All @@ -462,24 +449,24 @@ icon = html(t"""
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2"/>
</svg>
""")
assert '<svg width="24" height="24"' in str(icon)
assert '<circle cx="12" cy="12" r="10"' in str(icon)
assert '<svg width="24" height="24"' in icon
assert '<circle cx="12" cy="12" r="10"' in icon
```

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)
assert 'stroke="blue"' in str(result)
assert 'width="48"' in result
assert 'stroke="blue"' in result
```

#### Context
Expand All @@ -497,14 +484,14 @@ 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)
assert 'margin: 10px' in str(result)
assert '>Click me</button>' in str(result)
assert 'color: blue' in result
assert 'margin: 10px' in result
assert '>Click me</button>' in result
```

3. **Use module-level or global state**: For truly application-wide
Expand All @@ -518,41 +505,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
51 changes: 16 additions & 35 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.

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,23 +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>'
```

## Generators as Components

You can also have components that act as generators. For example, imagine you
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>"


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

## Nested Components
Expand All @@ -200,5 +181,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>'
```
8 changes: 4 additions & 4 deletions docs/usage/looping.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ result = html(
</ul>
"""
)
assert str(result) == """
assert result == """
<ul title="Hello">
<li>World</li><li>Universe</li>
</ul>
Expand All @@ -35,12 +35,12 @@ assert str(result) == """
## Rendered Looping

You could also move the generation of the items out of the "parent" template,
then use that `Node` result in the next template:
then use that 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
Loading