Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
840cceb
Minor docs changes
jealouscloud Sep 27, 2025
4700f1d
Typo fix
jealouscloud Sep 27, 2025
4775cc9
bugfix: emit true/false on bools.
jealouscloud Oct 11, 2025
ad7c717
attribute: Warn if user sends false and we evaluated
jealouscloud Oct 11, 2025
726157e
Test defer / bool arg behavior
jealouscloud Oct 11, 2025
355d147
Remove warning that would never fire
jealouscloud Oct 11, 2025
16c40dc
Remove dead attr resolve code
jealouscloud Oct 11, 2025
8f89b8c
Feature: html_compose.resource importer, streaming document generator
jealouscloud Oct 13, 2025
272afbd
Minor docs fixes
jealouscloud Oct 13, 2025
2264b85
Minor docstring fixes
jealouscloud Oct 13, 2025
9966bde
Redo the module docstring
jealouscloud Oct 14, 2025
db176d0
Small docs changes
jealouscloud Oct 14, 2025
b6d379b
Improvement: Translator extracts item from single element list
jealouscloud Oct 15, 2025
1919a8d
Bugfix: Use unsafe_text in translator when it preserves round trip
jealouscloud Oct 15, 2025
1d3c31c
Importer changelog
jealouscloud Oct 15, 2025
c28576c
mypy fixes
jealouscloud Oct 15, 2025
5846c26
Should just improve readability on this iterator
jealouscloud Oct 15, 2025
230723a
Remove unused import
jealouscloud Oct 15, 2025
3cfbf24
Pyright/Zed prefers google style docstrings
jealouscloud Nov 1, 2025
ea527a6
Improve watchcond docs
jealouscloud Nov 1, 2025
015342f
Improve livereload watcher
jealouscloud Nov 2, 2025
6588f3d
feature: live reload can now check server port for availability
jealouscloud Nov 2, 2025
e7d4e03
Fix broken markup
jealouscloud Nov 2, 2025
88b6a97
HTML5Document is class wrapping document.py functions
jealouscloud Nov 2, 2025
eb2ee94
breaking
jealouscloud Nov 2, 2025
d8ca8da
py.typed improvements
jealouscloud Nov 2, 2025
7e64470
bugfix: compare element to str
jealouscloud Nov 2, 2025
4b3e32a
You can now pass elements uninstantiated and we instantiate
jealouscloud Nov 7, 2025
596b5e6
Notebook render now accepts hashtml
jealouscloud Nov 7, 2025
3059aeb
Do not yield None
jealouscloud Nov 7, 2025
c311d4d
Fix deferred_resolve return type hint
jealouscloud Nov 7, 2025
e33546c
Inform type checker we expect a string
jealouscloud Nov 7, 2025
57bfd01
Prepare for release
jealouscloud Nov 10, 2025
d0fa083
mute mypy misreporting an assignment error
jealouscloud Nov 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,12 @@ a([tab]
```

* 🎭 Type hints for the editor generated from WhatWG spec
* ⚡ Live Reload server for rapid development

## Live Reload server for rapid development
Run your Python webserver (i.e. Flask, FastAPI, anything!) with live-reload superpowers powered by [livereload-js](https://www.npmjs.com/package/livereload-js). See browser updates in real-time!

Note: This feature requires optional dependencies. `pip install html-compose[live-reload]` or `pip install html-compose[full]`. The feature also fetches livereload-js from a CDN.
Note: This feature requires optional dependencies. `pip install html-compose[live-reload]` or `pip install html-compose[full]`.
The feature also loads livereload-js from a CDN.

`livereload.py`
```python
Expand Down
22 changes: 22 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
# 0.11.0
* Add html_compose.resource module which includes
js_import, css_import, and font_import helpers
* html_compose.document: Add streaming document generators which will produce
head element before other content.
* Add js/css/font composition along with other changes to html_compose.document
* [breaking] Change HTML5Document into a class with a .render and .stream function
which wrap other functionality in the module.
The signature is now different which may require updates.
* HTML converter improvements: Survive round trip for more text.
Output single elements when we identify an attribute could be a list
* Documentation improvements
* Migrate element/attr docstrings to Google style.
Pyright on Zed seems to tolerate this better.
* Fix a weird pylance bug on WatchCond
* Live reload now accepts daemon host/port/timeout and can wait to send browser
reload until after the server responds
* py.typed improvements to module exports
* Child elements passed uninstantiated will be instantiated at render time
* This means you can `p['line 1', br, 'line 2']` where previously you had to `br()`
* notebook.render accepts _HasHtml which includes documents and elements

# 0.10.1
* Style parameter: when input is a dict[str, str], assume the user wants simple f"{key}: {value}" css statements
* Type hints: Cleanup type hints to all be 3.10 style
Expand Down
22 changes: 16 additions & 6 deletions doc/ideas/03_code_generator.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
# Code Generator

Idea: What if your editor had built-in hinting around HTML properties?

## Implementation

We take information from the HTML spec living document and MDN.

We place generated code in a separated directory, keeping magic out of the code.

the `tools` directory contains:
* The spec generator `spec_generator.py`
* Pull, parse, and dump json we think is interesting
* The attribute code generator `generate_attributes.py`
* Put that interesting information in generated class attributes using our formula defined in previous specs. If an attribute is a keyword or restricted by Python, append `_` to the end of the name so autocomplete works. Dump those in `src/html_compose/attributes`

* `tools/generated/*_attrs.py` is the intermediate directory so runs do not write to source control directory, but you can see when a new change has happened.
- The spec generator `spec_generator.py`
- Pull, parse, and dump json we think is interesting
- The attribute code generator `generate_attributes.py`
- Put that interesting information in generated class attributes using our
formula defined in previous specs. If an attribute is a keyword or
restricted by Python, append `_` to the end of the name so autocomplete
works. Dump those in `src/html_compose/attributes`
- The element code generator `generate_elements.py`

- Same deal as for attributes. Dump in src/html_compose/elements.

- `tools/generated/*.py` is the intermediate directory so runs do not write to
the source control directory, but you can see when a new change has happened.

<!-- ## Extension of this idea:
TBD

caniuse data can be used to generate warnings based on browser targets -->
caniuse data can be used to generate warnings based on browser targets -->
8 changes: 4 additions & 4 deletions doc/ideas/04_attrs.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ div.hint.style({
The implementation is the simplest `<key>: <value>.
User is therefore responsible for quoting.

## `attrs=` parameter syntax
## attrs= parameter syntax

In the constructor for any element, you can specify the `attrs` parameter.

Expand Down Expand Up @@ -184,7 +184,7 @@ a(href="https://google.com", tabindex=1)
Under the hood, it's all translated to the `BaseAttribute` class, and the value is
escaped before rendering.

# Breakdown
## Breakdown

There are a number of options for declaring an attribute value, which are shown above.
The basic idea is
Expand Down Expand Up @@ -225,7 +225,7 @@ class htmx:
'''

@staticmethod
def hx_get(value: str) -> BaseAttribute:
def get(value: str) -> BaseAttribute:
'''
htmx attribute: hx-get
The hx-get attribute will cause an element to issue a
Expand All @@ -244,7 +244,7 @@ Where we can write

```python
button(
[htmx.hx_get("/api/data")],
[htmx.get("/api/data")],
class_="btn primary"
)["Click me!"]
```
2 changes: 1 addition & 1 deletion doc/ideas/05_livereload.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ If you're using nginx, you would include something like this in your server bloc
}
```

Since you're serving your websocket over SSL, you can now specify when calling `live.server(`:
Since you're serving your websocket over SSL, you can now specify when calling `live.server`:
* `proxy_host`: host to reach for the livereload websocket. used in browser instead of `host` parameter.

Example: `my-sweet-website.com`
Expand Down
263 changes: 263 additions & 0 deletions doc/ideas/06_resource_imports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Resource imports (js / css / fonts)

There are now many standards in the web platform for correctly importing
your remote resources.

We no longer need to use nodejs and bundling to use module import semantics.

We can preload js and assign it a module name for importing.

We give helpers for managing css/js imports and the many attributes needed
to successfully preload and validate resource integrity.

We also give two font helpers, one for fonts resolved in css and one for
manually setting up .woff/etc font imports.

## Browser tech overview

- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/modulepreload
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload

## How our library makes resource setup better

By providing small helpers, we reduce the amount of redundant html to write.

```python
import html_compose.elements as el
from html_compose.resource import css_import, js_import, to_elements
from html_compose.document import document_generator

def get_css():
return [
css_import("./static/admin.css", cache_bust=False),
css_import(
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css",
hash=(
"sha384-9ndCyUaIbzAi2FUVXJi0CjmCapS"
"mO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
),
crossorigin="anonymous",
preload=True,
),
]

def get_js():
return [
js_import("./static/admin.js", name="admin", cache_bust=True),
js_import(
"https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm",
name="alpinejs",
preload=True,
hash=(
"sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/"
"9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p"
),
crossorigin="anonymous",
),
]


def get_fonts():
return [
font_import_manual(
"./static/fonts/MyFont.woff2",
family="MyFont",
weight="normal",
style="normal",
display="swap",
cache_bust=False,
),
font_import_provider(
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap",
preconnect=[
"https://fonts.googleapis.com",
"https://fonts.gstatic.com",
],
preconnect_crossorigin="anonymous",
),
]



def test_importer():
css = get_css()
js = get_js()
elements = to_elements(js, css)
print(el.head()[elements].render())


def test_document_generator():
css = get_css()
js = get_js()
print(document_generator(
title="demo",
lang="en",
js=js,
css=css,
body_content=[el.h1("Hello world")])

```

**test_importer:**

```html
<head>
<link
href="https://fonts.googleapis.com"
crossorigin="anonymous"
rel="preconnect"
/>
<link
href="https://fonts.gstatic.com"
crossorigin="anonymous"
rel="preconnect"
/>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
as="style"
rel="preload"
/>
<link
href="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm"
integrity="sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p"
crossorigin="anonymous"
rel="modulepreload"
/>
<link
href="./static/fonts/MyFont.woff2"
type="font/woff2"
as="font"
rel="preload"
/>
<link href="./static/admin.css" rel="stylesheet" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&amp;display=swap"
rel="stylesheet"
/>
<style>
@font-face {
font-family: "MyFont";
src: url("./static/fonts/MyFont.woff2");
font-style: normal;
font-display: swap;
font-weight: normal;
}
</style>
<script type="importmap">
{
"imports": {
"admin": "./static/admin.js?ts=1760157623",
"alpinejs": "https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm"
}
}
</script>
<script src="./static/admin.js?ts=1760157623" type="module"></script>
<script
src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm"
type="module"
integrity="sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p"
crossorigin="anonymous"
></script>
</head>
```

**test_document**

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>demo</title>
<link
href="https://fonts.googleapis.com"
crossorigin="anonymous"
rel="preconnect"
/>
<link
href="https://fonts.gstatic.com"
crossorigin="anonymous"
rel="preconnect"
/>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
as="style"
rel="preload"
/>
<link
href="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm"
integrity="sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p"
crossorigin="anonymous"
rel="modulepreload"
/>
<link
href="./static/fonts/MyFont.woff2"
type="font/woff2"
as="font"
rel="preload"
/>
<link href="./static/admin.css" rel="stylesheet" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&amp;display=swap"
rel="stylesheet"
/>
<style>
@font-face {
font-family: "MyFont";
src: url("./static/fonts/MyFont.woff2");
font-style: normal;
font-display: swap;
font-weight: normal;
}
</style>
<script type="importmap">
{
"imports": {
"admin": "./static/admin.js?ts=1760157623",
"alpinejs": "https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm"
}
}
</script>
<script src="./static/admin.js?ts=1760157623" type="module"></script>
<script
src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm"
type="module"
integrity="sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p"
crossorigin="anonymous"
></script>
</head>

<body>
<h1>Hello world</h1>
</body>
</html>
```

## Where to go from here

We've informed the browser how to optimally and in parallel load our resources.

Now we are free to develop without bundling javascript if we so desire.

Heck, you could even map a package.json used in your development environment
into a `js_import` generator.

The sky is the limit.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "html-compose"
version = "0.10.1"
version = "0.11.0"
description = "Composable HTML generation in python"
authors = [
{ name = "jealouscloud", email = "github@noaha.org" }
Expand Down
Loading