|
1 | 1 | # html-tstring |
2 | 2 |
|
3 | | -Tools to manipulate and render HTML using Python 3.14's t-strings. |
| 3 | +A 🤘 rockin' t-string HTML templating system for Python 3.14. |
4 | 4 |
|
5 | | -Real documentation is forthcoming! |
| 5 | +## Installation |
6 | 6 |
|
7 | | -What can you do today? |
| 7 | +Just run: |
8 | 8 |
|
9 | | -1. Render HTML to an `Element` tree: |
| 9 | +```bash |
| 10 | +pip install html-tstring |
| 11 | +``` |
| 12 | + |
| 13 | +Python 3.14 isn't out yet, but you can use [Astral's `uv`](https://docs.astral.sh/uv/) to easily try `html-tstring` in a Python 3.14 environment: |
| 14 | + |
| 15 | +```bash |
| 16 | +uv run --with html-tstring --python 3.14 python |
| 17 | +``` |
| 18 | + |
| 19 | +## Usage |
| 20 | + |
| 21 | +`html-tstring` leverages Python 3.14's [new t-strings feature](https://t-strings.help/introduction.html) to provide a powerful HTML templating system that feels familiar if you've used JSX, Jinja2, or Django templates. |
| 22 | + |
| 23 | +T-strings work just like f-strings but use a `t` prefix and [create `Template` objects](https://docs.python.org/3.14/library/string.templatelib.html#template-strings) instead of strings. |
| 24 | + |
| 25 | +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. |
| 26 | + |
| 27 | +### Getting Started |
| 28 | + |
| 29 | +Import the `html` function and start creating templates: |
10 | 30 |
|
11 | 31 | ```python |
12 | 32 | from html_tstring import html |
| 33 | +greeting = html(t"<h1>Hello, World!</h1>") |
| 34 | +print(type(greeting)) # <class 'html_tstring.nodes.Element'> |
| 35 | +print(greeting) # <h1>Hello, World!</h1> |
| 36 | +``` |
| 37 | + |
| 38 | +### Variable Interpolation |
13 | 39 |
|
14 | | -template = t"<div><h1>Hello, world!</h1></div>" |
15 | | -element = html(template) |
16 | | -print(str(element)) |
17 | | -# <div><h1>Hello, world!</h1></div> |
| 40 | +Just like f-strings, you can interpolate (substitute) variables directly into your templates: |
| 41 | + |
| 42 | +```python |
| 43 | +name = "Alice" |
| 44 | +age = 30 |
| 45 | +user_info = html(t"<p>Hello, {name}! You are {age} years old.</p>") |
| 46 | +print(user_info) # <p>Hello, Alice! You are 30 years old.</p> |
18 | 47 | ``` |
19 | 48 |
|
20 | | -2. Get automatic escaping: |
| 49 | +The `html()` function ensures that interpolated values are automatically escaped to prevent XSS attacks: |
21 | 50 |
|
22 | 51 | ```python |
23 | | -from html_tstring import html |
| 52 | +user_name = "<script>alert('owned')</script>" |
| 53 | +safe_output = html(t"<p>Hello, {user_name}!</p>") |
| 54 | +print(safe_output) # <p>Hello, <script>alert('owned')</script>!</p> |
| 55 | +``` |
| 56 | + |
| 57 | +### Attribute Substitution |
| 58 | + |
| 59 | +The `html()` function provides a number of convenient ways to define HTML attributes. |
| 60 | + |
| 61 | +#### Direct Attribute Values |
24 | 62 |
|
25 | | -evil = "<script>alert('Hacked!');</script>" |
26 | | -template = t"<div>{evil}</div>" |
27 | | -element = html(template) |
28 | | -print(str(element)) |
29 | | -# <div><script>alert('Hacked!');</script></div> |
| 63 | +You can place values directly in attribute positions: |
| 64 | + |
| 65 | +```python |
| 66 | +url = "https://example.com" |
| 67 | +link = html(t'<a href="{url}">Visit our site</a>') |
| 68 | +# <a href="https://example.com">Visit our site</a> |
30 | 69 | ``` |
31 | 70 |
|
32 | | -3. Safely nest HTML elements: |
| 71 | +You don't _have_ to wrap your attribute values in quotes: |
33 | 72 |
|
34 | 73 | ```python |
35 | | -from html_tstring import html |
| 74 | +element_id = "my-button" |
| 75 | +button = html(t"<button id={element_id}>Click me</button>") |
| 76 | +# <button id="my-button">Click me</button> |
| 77 | +``` |
| 78 | + |
| 79 | +Boolean attributes are supported too. Just use a boolean value in the attribute position: |
36 | 80 |
|
37 | | -header = html(t"<header><h1>Welcome</h1></header>") |
38 | | -template = t"<div>{header}<p>This is the main content.</p></div>" |
39 | | -element = html(template) |
40 | | -print(str(element)) |
41 | | -# <div><header><h1>Welcome</h1></header><p>This is the main content.</p></div> |
| 81 | +```python |
| 82 | +form_button = html(t"<button disabled={True} hidden={False}>Submit</button>") |
| 83 | +print(form_button) |
| 84 | +# <button disabled>Submit</button> |
42 | 85 | ``` |
43 | 86 |
|
44 | | -4. Or, safely nest HTML templates: |
| 87 | +#### The `class` Attribute |
| 88 | + |
| 89 | +The `class` attribute has special handling to make it easy to combine multiple classes from different sources. The simplest way is to provide a list of class names: |
45 | 90 |
|
46 | 91 | ```python |
47 | | -from html_tstring import html |
| 92 | +classes = ["btn", "btn-primary", "active"] |
| 93 | +button = html(t'<button class="{classes}">Click me</button>') |
| 94 | +# <button class="btn btn-primary active">Click me</button> |
| 95 | +``` |
| 96 | + |
| 97 | +For flexibility, you can also provide a list of strings, dictionaries, or a mix of both: |
48 | 98 |
|
49 | | -header_template = t"<header><h1>Welcome</h1></header>" |
50 | | -template = t"<div>{header_template}<p>This is the main content.</p></div>" |
51 | | -element = html(template) |
52 | | -print(str(element)) |
53 | | -# <div><header><h1>Welcome</h1></header><p>This is the main content.</p></div> |
| 99 | +```python |
| 100 | +classes = ["btn", "btn-primary", {"active": True}, None, False and "disabled"] |
| 101 | +button = html(t'<button class="{classes}">Click me</button>') |
| 102 | +# <button class="btn btn-primary active">Click me</button> |
| 103 | +``` |
| 104 | + |
| 105 | +See the [`classnames()`](./html_tstring/classnames_test.py) helper function for more information on how class names are combined. |
| 106 | + |
| 107 | +#### The `style` Attribute |
| 108 | + |
| 109 | +In addition to strings, you can also provide a dictionary of CSS properties and values for the `style` attribute: |
| 110 | + |
| 111 | +```python |
| 112 | +# Style attributes from dictionaries |
| 113 | +styles = {"color": "red", "font-weight": "bold", "margin": "10px"} |
| 114 | +styled = html(t"<p style={styles}>Important text</p>") |
| 115 | +# <p style="color: red; font-weight: bold; margin: 10px">Important text</p> |
| 116 | +``` |
| 117 | + |
| 118 | +#### The `data` and `aria` Attributes |
| 119 | + |
| 120 | +The `data` and `aria` attributes also have special handling to convert dictionary keys to the appropriate attribute names: |
| 121 | + |
| 122 | +```python |
| 123 | +data_attrs = {"user-id": 123, "role": "admin"} |
| 124 | +aria_attrs = {"label": "Close dialog", "hidden": True} |
| 125 | +element = html(t"<div data={data_attrs} aria={aria_attrs}>Content</div>") |
| 126 | +# <div data-user-id="123" data-role="admin" aria-label="Close dialog" |
| 127 | +# aria-hidden="true">Content</div> |
| 128 | +``` |
| 129 | + |
| 130 | +Note that boolean values in `aria` attributes are converted to `"true"` or `"false"` as per [the ARIA specification](https://www.w3.org/TR/wai-aria-1.2/). |
| 131 | + |
| 132 | +#### Attribute Spreading |
| 133 | + |
| 134 | +It's possible to specify multiple attributes at once by using a dictionary and spreading it into an element using curly braces: |
| 135 | + |
| 136 | +```python |
| 137 | +attrs = {"href": "https://example.com", "target": "_blank"} |
| 138 | +link = html(t"<a {attrs}>External link</a>") |
| 139 | +# <a href="https://example.com" target="_blank">External link</a> |
| 140 | +``` |
| 141 | + |
| 142 | +You can also combine spreading with individual attributes: |
| 143 | + |
| 144 | +```python |
| 145 | +base_attrs = {"id": "my-link"} |
| 146 | +target = "_blank" |
| 147 | +link = html(t'<a {base_attrs} target="{target}">Link</a>') |
| 148 | +# <a id="my-link" target="_blank">Link</a> |
54 | 149 | ``` |
55 | 150 |
|
56 | | -TODO: write more examples, convert them into tests. |
| 151 | +Special attributes likes `class` behave as expected when combined with spreading: |
| 152 | + |
| 153 | +```python |
| 154 | +classes = ["btn", {"active": True}] |
| 155 | +attrs = {"class": classes, "id": "act_now", "data": {"wow": "such-attr"}} |
| 156 | +button = html(t'<button {attrs}>Click me</button>') |
| 157 | +# <button class="btn active" id="act_now" data-wow="such-attr">Click me</button> |
| 158 | +``` |
| 159 | + |
| 160 | +### Conditional Rendering |
| 161 | + |
| 162 | +You can use Python's conditional expressions for dynamic content: |
| 163 | + |
| 164 | +```python |
| 165 | +is_logged_in = True |
| 166 | +user_content = t"<span>Welcome back!</span>" |
| 167 | +guest_content = t"<a href='/login'>Please log in</a>" |
| 168 | +header = html(t"<div>{user_content if is_logged_in else guest_content}</div>") |
| 169 | +# <div><span>Welcome back!</span></div> |
| 170 | +``` |
| 171 | + |
| 172 | +Short-circuit evaluation is also supported for conditionally including elements: |
| 173 | + |
| 174 | +```python |
| 175 | +show_warning = False |
| 176 | +warning = t'<div class="alert">Warning message</div>' |
| 177 | +page = html(t"<main>{show_warning and warning}</main>") |
| 178 | +# <main></main> |
| 179 | +``` |
| 180 | + |
| 181 | +### Lists and Iteration |
| 182 | + |
| 183 | +Generate repeated elements using list comprehensions: |
| 184 | + |
| 185 | +```python |
| 186 | +fruits = ["Apple", "Banana", "Cherry"] |
| 187 | +fruit_list = html(t"<ul>{[t'<li>{fruit}</li>' for fruit in fruits]}</ul>") |
| 188 | +# <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul> |
| 189 | +``` |
| 190 | + |
| 191 | +### Raw HTML Injection |
| 192 | + |
| 193 | +The `html-tstring` package provides several ways to include trusted raw HTML content in your templates. This is useful when you have HTML content that you _know_ is safe and do not wish to escape. |
| 194 | + |
| 195 | +Under the hood, `html-tstring` builds on top of the familiar [MarkupSafe](https://pypi.org/project/MarkupSafe/) library to handle trusted HTML content. If you've used Flask, Jinja2, or similar libraries, this will feel very familiar. |
| 196 | + |
| 197 | +The `Markup` class from MarkupSafe is available for use: |
| 198 | + |
| 199 | +```python |
| 200 | +from html_tstring import html, Markup |
| 201 | + |
| 202 | +trusted_html = Markup("<strong>This is safe HTML</strong>") |
| 203 | +content = html(t"<div>{trusted_html}</div>") |
| 204 | +# <div><strong>This is safe HTML</strong></div> |
| 205 | +``` |
| 206 | + |
| 207 | +As a convenience, `html-tstring` also supports a `:safe` format specifier that marks a string as safe HTML: |
| 208 | + |
| 209 | +```python |
| 210 | +trusted_html = "<em>Emphasized text</em>" |
| 211 | +page = html(t"<p>Here is some {trusted_html:safe} content.</p>") |
| 212 | +# <p>Here is some <em>Emphasized text</em> content.</p> |
| 213 | +``` |
| 214 | + |
| 215 | +For interoperability with other templating libraries, any object that implements a `__html__` method will be treated as safe HTML. Many popular libraries (including MarkupSafe and Django) use this convention: |
| 216 | + |
| 217 | +```python |
| 218 | +class SafeWidget: |
| 219 | + def __html__(self): |
| 220 | + return "<button>Custom Widget</button>" |
| 221 | + |
| 222 | +page = html(t"<div>My widget: {SafeWidget()}</div>") |
| 223 | +# <div>My widget: <button>Custom Widget</button></div> |
| 224 | +``` |
| 225 | + |
| 226 | +TODO: support explicitly marking content as `unsafe` with a format specifier, too. |
| 227 | + |
| 228 | +### Template Composition |
| 229 | + |
| 230 | +You can easily combine multiple templates and create reusable components. |
| 231 | + |
| 232 | +Template nesting is straightforward: |
| 233 | + |
| 234 | +```python |
| 235 | +content = t"<h1>My Site</h1>" |
| 236 | +page = html(t"<div>{content}</div>") |
| 237 | +# <div><h1>My Site</h1></div> |
| 238 | +``` |
| 239 | + |
| 240 | +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: |
| 241 | + |
| 242 | +```python |
| 243 | +content = html(t"<h1>My Site</h1>") |
| 244 | +page = html(t"<div>{content}</div>") |
| 245 | +# <div><h1>My Site</h1></div> |
| 246 | +``` |
| 247 | + |
| 248 | +The result is the same either way. |
| 249 | + |
| 250 | +### Advanced Features |
| 251 | + |
| 252 | +#### Component Functions |
| 253 | + |
| 254 | +Create reusable template functions that work like custom HTML elements: |
| 255 | + |
| 256 | +```python |
| 257 | +def Alert(message: str, type: str = "info", **props) -> Template: |
| 258 | + alert_class = f"alert alert-{type}" |
| 259 | + attrs = {"class": alert_class, **props} |
| 260 | + return t'<div {attrs}>{message}</div>' |
| 261 | + |
| 262 | +# Use your component |
| 263 | +warning = html(t'<{Alert} message="Be careful!" type="warning" id="main-alert" />') |
| 264 | +``` |
| 265 | + |
| 266 | +#### Fragment Rendering |
| 267 | + |
| 268 | +Templates can return multiple root elements: |
| 269 | + |
| 270 | +```python |
| 271 | +def TableColumns() -> Template: |
| 272 | + return t"<td>Column 1</td><td>Column 2</td>" |
| 273 | + |
| 274 | +table = html(t"<table><tr><{TableColumns} /></tr></table>") |
| 275 | +``` |
| 276 | + |
| 277 | +The `html()` function returns a tree of nodes that can be converted to strings, manipulated programmatically, or composed with other templates for maximum flexibility. |
| 278 | + |
| 279 | +#### Context |
| 280 | + |
| 281 | +TODO: implement context feature |
0 commit comments