Skip to content

Commit 6d2e88d

Browse files
authored
Templating redux (#13)
BREAKING: Removes old v1 template support (deprecated in v1.4) and implements an all new templating system.
1 parent 97d2bbb commit 6d2e88d

9 files changed

Lines changed: 1006 additions & 110 deletions

File tree

README.md

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,81 +25,113 @@ E.p("Powered by:").br.a(href="...")("html5tagger")
2525
A complete example with template variables and other features:
2626

2727
```python
28-
from html5tagger import Document, E
28+
from html5tagger import Document, E, Template
29+
30+
# Create reusable templates
31+
Item = Template(E.li.Name("Item"))
2932

3033
# Create a document
3134
doc = Document(
32-
E.TitleText_, # The first argument is for <title>, adding variable TitleText
35+
"Demo", # The first argument is for <title>
3336
lang="en", # Keyword arguments for <html> attributes
3437

3538
# Just list the resources you need, no need to remember link/script tags
3639
_urls=[ "style.css", "favicon.png", "manifest.json" ]
3740
)
3841

39-
# Upper case names are template variables. You can modify them later.
40-
doc.Head_
41-
doc.h1.TitleText_("Demo") # Goes inside <h1> and updates <title> as well
42-
4342
# This has been a hard problem for DOM other such generators:
4443
doc.p("A paragraph with ").a("a link", href="/files")(" and ").em("formatting")
4544

45+
# Use templates to render dynamic content
46+
doc.h1("Demo")
47+
doc.ul._(Item(Name="Apple"), Item(Name="Banana"))
48+
4649
# Use with for complex nesting (not often needed)
4750
with doc.table(id="data"):
4851
doc.tr.th("First").th("Second").th("Third")
49-
doc.TableRows_
50-
51-
# Let's add something to the template variables
52-
doc.Head._script("console.log('</script> escaping is weird')")
52+
for row in range(3):
53+
doc.tr
54+
for col in range(3):
55+
doc.td(row * col)
5356

54-
table = doc.TableRows
55-
for row in range(10):
56-
table.tr
57-
for col in range(3):
58-
table.td(row * col)
59-
60-
# Or remove the table data we just added
61-
doc.TableRows = None
57+
# Add inline scripts or styles with special escaping
58+
doc.script("console.log('</script> escaping is weird')")
6259
```
6360

64-
You can `str(doc)` to get the HTML code, and using `doc` directly usually has the desired effect as well (e.g. giving HTML responses). Jupyter Notebooks render it as HTML. For debugging, use `repr(doc)` where the templating variables are visible:
61+
You can `str(doc)` to get the HTML code, and using `doc` directly usually has the desired effect as well (e.g. giving HTML responses). Jupyter Notebooks render it as HTML. For debugging, use `repr(doc)`:
6562

6663
```html
6764
>>> doc
6865
《Document Builder》
69-
<!DOCTYPE html><html lang=en><meta charset="utf-8">
70-
<title>《TitleText:Demo》</title>
66+
<!DOCTYPE html><html lang=en><meta charset="utf-8"><title>Demo</title>
7167
<link href="style.css" rel=stylesheet>
7268
<link href="favicon.png" rel=icon type="image/png">
7369
<link href="manifest.json" rel=manifest>
74-
《Head:<script>console.log('<\/script> escaping is weird')</script>》
75-
<h1>《TitleText:Demo》</h1>
7670
<p>A paragraph with <a href="/files">a link</a> and <em>formatting</em>
71+
<h1>Demo</h1>
72+
<ul><li>Apple<li>Banana</ul>
7773
<table id=data>
7874
<tr><th>First<th>Second<th>Third
79-
《TableRows》
75+
<tr><td>0<td>0<td>0
76+
<tr><td>0<td>1<td>2
77+
<tr><td>0<td>2<td>4
8078
</table>
79+
<script>console.log('<\/script> escaping is weird')</script>
8180
```
8281

8382
The actual HTML output is similar. No whitespace is added to the document, it is all on one line unless the content contains newlines. You may notice that `body` and other familiar tags are missing and that the escaping is very minimal. This is HTML5: the document is standards-compliant with a lot less cruft.
8483

85-
## Templating (v1 deprecated)
84+
## Templating
8685

87-
> ⚠️ **Deprecation notice:** The v1.3 templating API is deprecated as of html5tagger 1.4 and will be removed in 2.0. If you rely on it, pin `html5tagger<2` in your dependencies. Otherwise, upgrade to html5tagger 2.0 for the new templating API.
86+
A document builder can be turned into a template by `Template(doc)`. Templates prebuild all static content as long strings, leaving only capitalized placeholders to be filled in at render time. This provides extremely fast rendering and allows building a complex page out of clean components.
8887

89-
The old API lets you mutate template tags inside a `Builder` and later render the document. html5tagger 2.0 replaces this with immutable `Template` objects that you render by calling them with the desired slot values. Placeholders no longer use an underscore suffix: `doc.TagName` adds the placeholder to the document (in v1 `doc.TagName_` did so), and `doc.TagName(value)` sets a default. To migrate, remove the underscore and use `Template(doc)` to compile your document into a static template that can be called with `TagName=` keyword arguments to render HTML output.
88+
The example below defines a page with `Title` reused for both the `<title>` and `<h1>`, and an `Items` list populated from a product list. Parentheses directly after a placeholder set its default value (empty by default).
89+
90+
```python
91+
from html5tagger import Document, E, Template
92+
93+
# Define the reusable templates once
94+
Page = Template(Document(E.Title).h1.Title.ul.Items)
95+
Item = Template(E.li.span[".name"].Name._(": ").span[".price"].Price("N/A"))
96+
97+
# Super fast rendering just fills in the dynamic data
98+
def render(products: list) -> str:
99+
return Page(
100+
Title="Product List",
101+
Items=[Item(**product) for product in products],
102+
)
103+
104+
html = render([
105+
{"Name": "Apple", "Price": "$1.20"},
106+
{"Name": "Banana"},
107+
])
108+
```
109+
110+
```html
111+
<!DOCTYPE html>
112+
<meta charset="utf-8">
113+
<title>Product List</title>
114+
<h1>Product List</h1>
115+
<ul>
116+
<li><span class=name>Apple</span>: <span class=price>$1.20</span>
117+
<li><span class=name>Banana</span>: <span class=price>N/A</span>
118+
</ul>
119+
```
120+
121+
A builder can be finalized into a template by `Template(...)`. The resulting `Template` object is immutable and is called with keyword arguments to render the placeholders. Template values follow the same escaping rules as `doc(...)`, and a list of builders or strings is expanded in place.
90122

91123
## Nesting
92124

93125
In HTML5 elements such as `<p>` do not need any closing tag, so we can keep adding content without worrying of when it should close. This module does not use closing tags for any elements where those are optional or forbidden.
94126

95-
A tag is automatically closed when you add content to it or when another tag is added. Setting attributes alone does not close an element. Use `(None)` to close an empty element if any subsequent content is not meant to go inside it, e.g. `doc.script(None, src="...")`.
127+
A tag is automatically closed when you add content to it or when another tag is added. Setting attributes alone does not close an element, so we can do `doc.div[".foo"]("inside")` where the content still goes inside the div. `None` may be passed for content to close without content, e.g. `doc.div(None)("after")` produces `<div></div>after`.
96128

97129
For elements like `<table>` and `<ul>`, you can use `with` blocks, pass sub-snippet arguments, or add a template variable.
98130

99131
```python
100132
with doc.ul: # Nest using with
101133
doc.li("Write HTML in Python")
102-
doc.li("Simple syntax").ul(id="inner").InnerList_ # Nest using template
134+
doc.li("Simple syntax").ul(id="inner").InnerList # Nest using template
103135
doc.li("No need for brackets or closing tags")
104136
doc.ul(E.li("Easy").li("Peasy")) # Nest using (...)
105137
```
@@ -126,9 +158,7 @@ Works perfectly in browsers.
126158

127159
## Name mangling and boolean attributes
128160

129-
Underscore at the end of name is ignored so that `for_` and other attributes may be used despite being reserved words in Python. Other underscores convert into hyphens.
130-
131-
⚠️ The above only is true for HTML elements and attributes, but template placeholders only use an ending underscore to denote that the it is to be placed on the document, rather than be fetched for use.
161+
Underscore at the end of a name is ignored so that `for_` and other attributes may be used despite being reserved words in Python. Other underscores convert into hyphens.
132162

133163
Boolean values convert into short attributes.
134164

@@ -212,9 +242,9 @@ In the above benchmark html5tagger created the entire document from scratch, one
212242

213243
## Further development
214244

215-
There have been no changes to the tagging API since 2018 when this module was brought to production use, and thus the interface is considered stable.
245+
There have been no changes to the tagging API since 2018 when this module was brought to production use, and thus the interface is considered stable with only small incremental changes like the `script` and `style` special methods being added.
216246

217-
The legacy templating API added as a draft in version 1.3 is deprecated as of version 1.4 and will be removed in 2.0, where it is replaced by a redesigned templating system. Users who depend on the old templating behaviour should pin `html5tagger<2`; all others are encouraged to upgrade to 2.0.
247+
The templating API added as a draft in version 1.3 is deprecated as of version 1.4 and is removed in 2.0, where it is replaced by a redesigned templating system. Users who depend on the old templating behaviour should pin `html5tagger<2`; all others are encouraged to upgrade to 2.0 which is faster and more versatile.
218248

219249
## Development
220250

html5tagger/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
"""Generate HTML5 documents directly from Python code."""
1+
"""Generate HTML5 documents directly in Python."""
22

33
from importlib.metadata import version
44

5-
__all__ = "Builder", "Document", "E", "HTML"
5+
__all__ = "Builder", "Document", "E", "HTML", "Template"
66
__version__ = version("html5tagger")
77

88
from . import builder, document, makebuilder, util
99
from .builder import Builder
1010
from .document import Document
1111
from .makebuilder import E
12+
from .template import Template
1213
from .util import HTML

0 commit comments

Comments
 (0)