From 40f8420fe9bee1f1aa4f1bc3c804405ca32417d3 Mon Sep 17 00:00:00 2001 From: "L. Karkkainen" Date: Sun, 5 Jul 2026 04:50:10 +0000 Subject: [PATCH 01/15] Clean up templating implementation - Remove mutable template-tag leftovers: - Builder.render(), _render_piece(), _render_value() - Builder.__setattr__() slot setter - trailing-underscore handling for placeholders - debug _optimize() method - Update tests to use immutable Template objects only - Update README and Template docstrings for new API - Fix benchmark unused imports and formatting --- README.md | 68 +++++++++++--------- html5tagger/__init__.py | 3 +- html5tagger/builder.py | 61 +++++++----------- html5tagger/template.py | 91 +++++++++++++++++++++++++++ pyproject.toml | 1 + scripts/benchmark.py | 95 ++++++++++++++++++++++++++++ tests/test_html5tagger.py | 17 ----- tests/test_templating.py | 127 +++++++++++++++++++++++++++++--------- 8 files changed, 347 insertions(+), 116 deletions(-) create mode 100644 html5tagger/template.py create mode 100644 scripts/benchmark.py diff --git a/README.md b/README.md index ef6a353..e042d3d 100644 --- a/README.md +++ b/README.md @@ -25,66 +25,76 @@ E.p("Powered by:").br.a(href="...")("html5tagger") A complete example with template variables and other features: ```python -from html5tagger import Document, E +from html5tagger import Document, E, Template + +# Create reusable templates +Item = E.li.Name("Item") @ Template # Create a document doc = Document( - E.TitleText_, # The first argument is for , adding variable TitleText + "Demo", # The first argument is for <title> lang="en", # Keyword arguments for <html> attributes # Just list the resources you need, no need to remember link/script tags _urls=[ "style.css", "favicon.png", "manifest.json" ] ) -# Upper case names are template variables. You can modify them later. -doc.Head_ -doc.h1.TitleText_("Demo") # Goes inside <h1> and updates <title> as well - # This has been a hard problem for DOM other such generators: doc.p("A paragraph with ").a("a link", href="/files")(" and ").em("formatting") +# Use templates to render dynamic content +doc.h1("Demo") +doc.ul._(Item(Name="Apple"), Item(Name="Banana")) + # Use with for complex nesting (not often needed) with doc.table(id="data"): doc.tr.th("First").th("Second").th("Third") - doc.TableRows_ + for row in range(3): + doc.tr + for col in range(3): + doc.td(row * col) -# Let's add something to the template variables -doc.Head._script("console.log('</script> escaping is weird')") - -table = doc.TableRows -for row in range(10): - table.tr - for col in range(3): - table.td(row * col) - -# Or remove the table data we just added -doc.TableRows = None +# Add inline scripts or styles with special escaping +doc._script("console.log('</script> escaping is weird')") ``` -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: +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)`: ```html >>> doc 《Document Builder》 -<!DOCTYPE html><html lang=en><meta charset="utf-8"> -<title>《TitleText:Demo》 +Demo -《Head:》 -

《TitleText:Demo》

A paragraph with a link and formatting +

Demo

+
FirstSecondThird - 《TableRows》 +
000 +
012 +
024
+ ``` 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. ## Templating -Use template variables to build a document once and only update the dynamic parts at render time for faster performance. Access template variables via doc.TitleText and add content in parenthesis after the tag name. The underscore at the end of a tag name indicates the tag is added to the document and can have content in parenthesis, but any further tags on the same line go to the original document, not the template. +Use `Template` objects to build a document once and only fill in the dynamic parts at render time for faster performance. Uppercase names inside a builder become named slots. Wrap a builder with `Template` (or use the `@` operator) to create an immutable, callable template. Calling the template with keyword arguments renders the slots and returns the HTML string. + +```python +from html5tagger import E, Template + +Item = E.li.Name("unknown") @ Template + +str(Item(Name="Apple")) #
  • Apple +str(Item()) #
  • unknown +``` + +Template slot values are escaped by default. Pass an `HTML` object or any object with an `__html__` method to include preformatted HTML. ## Nesting @@ -97,7 +107,7 @@ For elements like `` and `