Skip to content

koxudaxi/tstring-html

Repository files navigation

T-strings for HTML

CI PyPI - html-tstring PyPI - thtml-tstring Python 3.14+ License: MIT

Maintainer update: Open to opportunities. koxudaxi.dev

Parser-first HTML and T-HTML backends for PEP 750 template strings.

Documentation: tstring-html.koxudaxi.dev

Python 3.14 introduces t-strings. They look like f-strings but give you structured access to the interpolation values instead of concatenating them into a string. This project uses that structure to parse the template as HTML first, then insert escaped values into the right slots. The result is always valid HTML, and XSS is not possible.

Packages

Package What it does
html-tstring Plain HTML rendering with auto-escaping
thtml-tstring Adds JSX-style component tags on top of html-tstring

tstring-html-bindings (the native extension) is pulled in automatically.

Installation

Requires Python 3.14+.

pip install html-tstring
pip install thtml-tstring

Or with uv:

uv add html-tstring
uv add thtml-tstring

Pre-built wheels are available for Linux x86_64, macOS Apple Silicon, and Windows x86_64. On other platforms you need Rust 1.94.0+ to build tstring-html-bindings from source.

Quick start

Safe HTML rendering

Interpolated values are HTML-escaped automatically:

from html_tstring import render_html

name = "<script>alert('xss')</script>"
page = render_html(t"<div class='greeting'>Hello, {name}!</div>")
# <div class='greeting'>Hello, &lt;script&gt;alert('xss')&lt;/script&gt;!</div>

Class normalization

The class attribute accepts strings, lists, and conditional dicts, similar to clsx in the JS ecosystem:

from html_tstring import render_html

classes = ["btn", {"btn-primary": True, "btn-disabled": False}]
page = render_html(t'<button class="{classes}">Click</button>')
# <button class="btn btn-primary">Click</button>

Spread attributes

Pass a dict as a bare interpolation to spread it across the tag:

from html_tstring import render_html

attrs = {"data-id": "42", "hidden": False, "class": "extra"}
page = render_html(t'<div class="base" {attrs}>content</div>')
# <div class="base extra" data-id="42">content</div>

What is T-HTML?

T-HTML is a small DSL on top of t-strings. It adds one rule: a tag whose name starts with an uppercase letter (e.g. <Card>) is treated as a component call. The tag name is looked up as a Python callable, attributes become keyword arguments, and nested content is normalized and passed as children.

There is no virtual DOM, no state management, no build step. It is just a way to write reusable HTML fragments as functions and compose them with familiar <Tag> syntax inside t-strings.

from string.templatelib import Template
from thtml_tstring import component, thtml

@component
def Card(*, children: str, title: str) -> Template:
    return t"""
        <div class="card">
          <div class="card-header"><h3>{title}</h3></div>
          <div class="card-body">{children}</div>
        </div>
    """

@component
def Badge(*, children: str, tone: str = "info") -> Template:
    return t'<span class="badge badge-{tone}">{children}</span>'

@component
def Button(*, children: str, **props: object) -> Template:
    return t'<button {props}>{children}</button>'

# Compose them like JSX
user = "Alice"
status = "active"
result = thtml(t"""
<Card title='User Profile'>
  <p>Name: {user}</p>
  <p>Status: <Badge tone='success'>{status}</Badge></p>
  <Button type='submit'>Save</Button>
</Card>
""")

html = result.render()

The @component decorator wraps Template return values into a Renderable automatically. You can also create a Renderable explicitly with thtml() when you need to control scope or backend:

from thtml_tstring import Renderable

@component
def Badge(*, children: str, tone: str = "info") -> Renderable:
    # explicit wrap, equivalent to the auto-wrap above
    return thtml(t'<span class="badge badge-{tone}">{children}</span>')

RawHtml still exists for injecting external trusted HTML strings, but it is no longer needed for component composition.

Components are resolved from the caller's scope by default. For tests or framework integration you can pass the scope explicitly:

thtml(
    t"<Button>Save</Button>",
    globals={"Button": my_button_component},
    locals={},
)

Editor integration (t-linter)

t-linter is a linter, formatter, and LSP server for t-strings. It uses the same Rust backends as this project for check and format.

pip install t-linter

Check templates for errors:

t-linter check src/

Format HTML / T-HTML template literals:

t-linter format src/

Start the LSP server for real-time editor diagnostics:

t-linter lsp

A VSCode extension is also available.

Documentation

Full docs: tstring-html.koxudaxi.dev

Practical examples

The repository includes typed examples that show how these APIs look in real code, including spread attrs, Renderable composition, @component auto-wrap, and explicit thtml(...) usage:

Conformance

This repository makes repo-local v1 conformance claims rather than claiming full coverage of the external HTML ecosystem.

  • HTML default profile: 34 manifest cases
  • T-HTML default profile: 47 manifest cases

The current matrix covers parser/backend seam behavior, Renderable composition, scope capture, formatter raw_source fidelity, semantic spans, and runtime boundaries. See the live status page:

Development

uv sync --group dev
cargo test --manifest-path rust/Cargo.toml --workspace --tests
uv run pytest -q
uv run coverage run -m pytest -q && uv run coverage report
uv run ruff check .

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages