html_compose

html-compose

A library for natural HTML composition directly in Python.

Focused on fast, flexible, extensible document generation, its goal is to make the web platform fun to work with while using modern browser technologies.

Quick Start

All HTML elements from the living spec are available to use with full type hinting:

from html_compose import a

element = a(href="/logout")["Log out"]
print(element.render())
# <a href="/logout">Log out</a>

The [] syntax provides a natural way to define child elements, making the code resemble the HTML structure it represents.

Behind the scenes, this is .base_element.BaseElement.append, which accepts text, elements, lists, nested lists, and callables. It returns self for chaining.

Think of it as:

  • () sets attributes
  • [] adds children

Set non-constructor attributes with a dict:

a({"@click": "alert(1)"}, href="#")["Click me"]

Security: The children of HTML elements are always HTML escaped, so XSS directly in the HTML is not possible.

JavaScript within HTML attribute values is always escaped. Just don't pass user input into JavaScript attributes.

Use .unsafe_text() when you need unescaped content.

All HTML nodes treat their children as if they contain HTML. This means if you have a <script> or <style> element or something else that isn't read as HTML, you may need to handle escaping yourself before passing to .unsafe_text().

Imports

You can import elements from this module or html_compose.elements:

Building Documents

Use document_generator for complete HTML5 documents with optimized resource management:

from html_compose import p
from html_compose.document import document_generator
from html_compose.resource import js_import, css_import

# Local module with cache-busting
admin_script = js_import(
    "./static/admin.js",
    name="admin",
    cache_bust=True,
    preload=True
)

# Remote library
alpine = js_import(
    'https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js',
    defer=True
)

# CSS with integrity checking and preload
bootstrap_css = css_import(
    "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css",
    preload=True,
    hash="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM",
    crossorigin="anonymous"
)

doc: str = document_generator(
    title="My App",
    css=[bootstrap_css],
    js=[admin_script, alpine],
    head_extra=[],  # anything you want to add to head
    body_content=[
        p(class_="container")["Hello, world!"]
    ]
)

Generates HTML with correct <link>, <script>, importmap, and preload tags in the optimal order.

For streaming responses, use document_streamer to send the <head> early.

Basic Document API

If you prefer simpler boilerplate generation, HTML5Document returns a complete document string:

from html_compose import HTML5Document, p, script, link, meta

doc: str = HTML5Document(
    "Site Title",
    lang="en",
    js=[
        '/public/bundle.js',
    ],
    css=[
        '/public/style.css',
    ],
    head_extra=[
        meta(name='robots', content='index, follow'),
    ],
    body=[p["Hello, world!"]],
)

See html_compose.resource for advanced resource configuration, or view html_compose.document for for more on document generation.

Composing Elements

The constructor for an element defines attributes, so if it has none the call to the constructor can be skipped like the p and strong elements below:

from html_compose import div, strong, a

user = "github wanderer"
content = div(class_="profile")[
    p["Welcome, ", strong[user], "!"],
    a(href="/logout")["Log out"]
]

print(content.render())
# <div class="profile"><p>Welcome, <strong>github wanderer</strong>!</p><a href="/logout">Log out</a></div>

More Features

Custom Elements

Create custom elements with CustomElement.create or create_element:

from html_compose.custom_element import CustomElement

foo = CustomElement.create("foo")
foo["Hello world"].render()  # <foo>Hello world</foo>

# Or use the shorthand
from html_compose import create_element

bar = create_element("bar")
bar()["Hello world"].render()  # <bar>Hello world</bar>

Type Hints

All elements and attributes are fully type-hinted for IDE support. Your editor can complete element names and attributes.

Flexible Attributes

Attributes support multiple input formats:

img([img.hint.src("..."), {"@click": "..."}, onmouseleave="..."])

Extensions

Custom attributes for frameworks can be packaged as reusable modules:

from pretend_extensions import htmx
from html_compose import button, div

button([htmx.get('/url'), htmx.target('#result')])
# <button hx-get="/url" hx-target="#result"></button>

Command-line Interface

Convert HTML to html-compose syntax—useful when starting from tutorials or templates:

html-compose convert {filename or empty for stdin}
html-compose convert --noimport el  # produces el.div() style references
html-convert # an alias for html-compose convert

html-convert provides access to this tool as shorthand.

Core Ideas

We are going to dive into the technicals and core ideas of the library.

String Iterators and Tree Resolution in HTML Generation

Core Concepts

  1. Generators are beneficial to performance
  • Elements are generated one at a time and immediately consumed by the join operation. You never store the complete set in memory.
  • Each element is processed exactly once as it flows through the generator pipeline.
  • Lazy evaluation: The HTML is generated on-demand, which is particularly valuable when building conditional elements or working with large datasets. -
  1. String Iterators:
  • Used as the output of the __html__ method, which represents sanitized elements and markup which is implemented as deferred_resolve in our base element class,
  1. Tree Resolution: The process of walking through the HTML element tree and resolving all children.

Concept: Iterator flattening

An iterator can contain iterators, like this:

ul[ (li["one"], li["two"]) ]

This allows syntax like this:

def get_items(db):
        return (
            li[
                h[row.name],
                h2[row.type]
                p[row.value]
            ] for row in db.query.stuff("select name, type, value ...")
    )
ul[ get_items(db_session) ]

Concept: Tree Resolution

To resolve element.children into a list of strings, we first have to recursively walk the html element tree and yield all resolved children

We want to resolve the following in two ways.

def get_article_content(_id):
    return "my article content"

article[
    h1["My cool article"],
    p(id="article-1")[
        lambda p: database.get_article_content(p.id)
    ]
]

Deferred resolve

The deferred resolve step will resolve an iterator that looks a lot like this, as a list:

[
    "<article>",

    "<h1>", "My cool article", "</h1>",

    "<p id='article-1'>",
    lambda: database.get_article_content()
    "</p>",

    "</article>"
]

As you can see, the static content that resolves is returned, but callables are returned as themselves.

Full resolution

We simply walk the iterator generated in the deferred resolve step and call any callables before returning to str.join.

We believe a few cool things can be done with this regarding content generation and rendering.

The snippet above turns into

"<p>",
  "my article content"
"</p>",

Basic HTML iterator

The deferred_resolve method in our BaseElement class is a generator that yields HTML strings is similar to the following:

def deferred_resolve(self):
    attrs = self.resolve_attrs()
    children = None if self.is_void_element else [child for child in self.resolve_tree()]

    # Yield opening tag, children, and closing tag
    yield f"<{self.name} {attrs}>"
    if children:
        yield from children
    yield f"</{self.name}>"

Base Element

The base element has tricks built into it so that you can write HTML faster.

Concepts

[] Syntax for Children

[] syntax is a wrapper for Element.append i.e. div().append, except that it returns itself so that it can be chained: div[ div["a", "b", "c"] ]

Under the hood, this is just BaseElement.__getitem__

[] Syntax Constructor

Normally you would construct an element like h1()["My header"].

You can also do h1["My header"].

You will notice this bypasses running the constructor.

Under the hood, this is just BaseElement.__class_getitem__ which just runs the constructor with no parameters. This shorthand can save a few keystrokes.

Iterators / Callables

You can nest iterators and even place callables in your children. i.e. div()[lambda: "evaluated at render time", [ br(), "text" ]]

Lambda Parameters

multi-parameter lambda function: div()[lambda node, parent_if_avail: "Demo"]

  • 0 params: nothing
  • param 1: parent node, if applicable * param 2: its parent, if applicable

Element attributes aren't currently intended to be accessed to prevent mistreating HTML as data or application state, so these parameters likely be more useful in a custom extension of an element.

Attribute Classes

You can access Element.attribute i.e. img.srcset() with description, implemented as classes which are chldren of subclass. These can be passed in element initialization a(attrs=[a.href("https://google.com")]) and has the benefit of auto-complete.

LRU Cache

  • The basic attribute concatenation functions are called a lot and so they maintain an LRU cache configurable in size via Element.ATTR_CACHE_SIZE i.e. div.ATTR_CACHE_SIZE. This works because it can guarantee it is only working on strings.
  • The multi-parameter lambda function also has an LRU cache to reduce time spent getting function parameters.

Name Conflicts

Examples:

  • class_
  • del_

You can't use class as an argument in Python because it is a keyword. We opt to call it class_

This alternative was chosen because it is identical to autocomplete.

The same rule is applied anywhere else a name conflicts with a keyword i.e. the del element.

Repeat Attributes

In the event class or style occur multiple times, they are concatenated with the correct delimiter in the order they're received.

Because there's no clear way to concat other attributes, an exception is raised.

XSS Prevention / Automatic Escape

Unescaped child nodes i.e. strings are automatically escaped to prevent XSS.

This is done by the PalletsProjects markupsafe library.

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
  • 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.

Attributes

The goal of the library is to enable the user to make design choices about how they generate their HTML.

Therefore, the library proposes several ways to define attributes for an HTML element.

The theory is that creating the least resistance to a successful pattern makes way for its adoption.

from html_compose import div
is_error = False

# keyword arg syntax (preferred)
# note that attributes that conflict with Python keywords
# have an underscore_ appended. This was chosen so autocomplete still works.

div(class_="flex")
div(class_=["flex", "items-center"])
div(class_={
    "flex": True,
    "error": is_error
})
div([div.hint.class_("flex")])

div([div._.class_("flex")])
# div._ is a syntax shorthand for div.hint

# attrs dict syntax
div(attrs={"class": "flex"})
div(attrs={"class": ["flex"]})
div(attrs={"class": {
    "flex": True,
    "error": is_error
}})

# Also technically works
div(attrs={"class": div.hint.class_("flex")})

# attrs list syntax
div(attrs=[div.hint.class_("flex")])
div(attrs=[div.hint.class_(["flex", "items-center"])])
div(attrs=[div.hint.class_({
    "flex": True,
    "error": is_error == True
})])

# Combining the two:
div(attrs=[div.hint.class_("flex")], tabindex=1)

BaseAttribute

All attributes inherit from BaseAttribute, which defines a key and a value and resolves at render time.

The class and style attributes have special rules to join values with their correct delimiter.

from html_compose import div
is_red = False
# dict of str:bool - if the value is true, the key is rendered as part of the class list
# truthy = rendered
# falsey = ignored

div.hint.class_({
        'red': is_red,
        'blue': not is_red
    }
)
# "blue"

# list of values (joined by whitespace)
div.hint.class_(["red", "center"])
# "red center"

div._.class_("red")
# "red"

An easy mistake is getting caught assuming the dictionary will resolve the value

from html_compose import div

# This is NOT the correct way to use a dictionary
div.hint.class_({
        'color': "red", # ❌ Incorrect use
    }
)
# "color" ❌ is likely not what you wanted

An exception to the rule is style

from html_compose import div

# the style attribute has special handling.
div.hint.style({
        'background': "red", # OK
        "flow-direction": "row"
    }
)
"background: red; flow-directionn: row"

The implementation is the simplest `: . User is therefore responsible for quoting.

attrs= parameter syntax

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

It can be either a list or a dictionary.

Implicit/positional attrs argument

Although the documentation is explicit in using the attrs kwarg, attrs is actually the first argument of the constructor and its name can be omitted.

div({"class": "flex"})

Instead of

div(attrs={"class": "flex"})

list

It supports a list of BaseAttributes but also you can mix a dictionary in as well.

from html_compose.elements import a, div

div(attrs=[div.class_("red")])

a(attrs=[
    a.hint.href("https://google.com"),
    a.hint.tabindex(1),
    a.hint.class_(["flex", "flex-col"])
])


a(attrs=[
    {"@custom": "value"},
    a.hint.href("https://google.com"),
    a.hint.tabindex(1),
    a.hint.class_(["flex", "flex-col"])
])

# string / list of string is explicitly NOT supported
# it requires disabling sanitization and is therefore quietly prone to XSS
div(attrs=['class="red"']) # ❌

dict

a(attrs={
    "href": "https://google.com"),
    "tabindex": 1
})

div(attrs={
    "class": "red"
})



div(attrs={
    "class": ["flex", "items-center"]
})

Keyword argument extension

An extension of the attrs syntax was generated for all built-in HTML elements. It would be time-consuming to do this for custom element types, but code generation lends itself well to this case.

Traditionally, kwargs would be too non-descript to provide helpful editor hints.

To aid with fluent document writing, each element was generated with its attributes as parameters and a paired docstring. i.e. :param href: Address of the hyperlink

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

There are a number of options for declaring an attribute value, which are shown above. The basic idea is

attrs, the first parameter, is a key,value attribute set, or a list containing one or more of

  • a dict that translates key="{safe_text(value)}", as if attrs were a dict
  • BaseAttribute which may be from a hint class for an element or library

Attribute definitions

Care was put into generating attribute definitions for each class.

Anything found in the HTML specification document is available in an element's cousin attribute class.

i.e. the img class has a cousin class ImgAttrs.

We can access the definition of an attribute for that element via ImgAttrs.$attr i.e. ImgAttrs.alt(value="demo"). Each element, like img, has a child class hint which inherits from its sibling attrs class (ImgAttrs), so you can access the same definition via img.hint.alt("...").

Additionally, there's a _ shorthand for img.hint. img._ is just a reference to img.hint.

The purpose of this system is to provide full type hints.

It also serves as an example for extensions to add attribute sets under their own namespaces/classes.

Extensions

Quality extensions are recommended to work with your chosen tech stack. The idea is to give you guardrails and documentation directly in your IDE.

from html_compose.base_attribute import BaseAttribute
from html_compose import button
class htmx:
    '''
    Attributes for the HTMX framework.
    '''

    @staticmethod
    def get(value: str) -> BaseAttribute:
        '''
        htmx attribute: hx-get
            The hx-get attribute will cause an element to issue a
            GET to the specified URL and swap the HTML into the DOM
            using a swap strategy

        :param value: URI to GET when the element is activated
        :return: An hx-get attribute to be added to your element
        '''

        return BaseAttribute("hx-get", value)

Where we can write

button(
    [htmx.get("/api/data")], 
    class_="btn primary"
)["Click me!"]

Live Reload

If it takes multiple steps to make a change, I'm going to make a change slowly.

If I have to do nothing to notice my change immediately, I'm going to make changes quickly.

The idea is to iterate rapidly, so we provide a generic tool to help you do just that.

Live reload is an optional feature of html-compose to aid in rapid development.

Browser based livereload is provided by livereload-js.

To trigger it, we host a websocket server on port 51353 by default.

Our library comes optionally equipped with a file watcher that derives certain actions from file events.

It can be used to run your webserver and build commands in certain events.

Path expressions

Paths expressions essentially globs as you understand them from glob.glob with recursive=True.

Ignore_glob and path_glob use the same mechanism.

Regular glob

They support regular file globbing where * is 1 or more characters i.e. *.py

Recursive glob

They support recursion via ** which matches any 0 or more directories:
src/**/*.py will match both src/demo.py and src/my/nested/dir/demo.py.

Trailing / (recursive)

A trailing / will also be interpreted as a recursive match i.e.

src/ will match src/any/file.txt

Example usage

This demonstrates a Flask application uses a combination of vanilla js and bundled node dependencies.

live.py

import html_compose.live as live

live.server(
    daemon=live.ShellCommand(
        "flask --app ./src/web_demo/server.py run"
    ),
    daemon_delay=1,
    # These conditions determine when to reload the flask app
    # And commands to run based on the matching condition
    conds=[
        live.WatchCond(
            # Trigger reload when a python file changes
            path_glob="src/**/*.py",
            ignore_glob="src/.venv/"
            action=None,
        ),
        live.WatchCond(
            "node-app/**/*.js",
            action=live.ShellCommand("./build.sh"),
            # no reload means not to try to reload the daemon or browser
            reload=False,
        ),
        live.WatchCond(
            # Trigger reload when the bro
            "public/**/*.js",
            action=None,
        ),
    ],
    host="localhost",
    port=51353,
)

build.sh

#!/usr/bin/env bash
(cd node-app && (
    ./node_modules/.bin/esbuild ./myapp.js --bundle --outfile=../public/node-app.js
  )
)

running:

[~/src/mine/web-demo]$ python3 livereload.py 
Monitoring for changes: src
Monitoring for changes: node-app
Monitoring for changes: public
Monitoring 3 path(s) via RustNotify. 3 path(s) are monitored recursively.
Starting livereload WebSocket server at ws://localhost:51353
 * Serving Flask app './src/web_demo/server.py'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
Changed: node-app/myapp.js

  ../public/node-app.js  316.6kb

⚡ Done in 16ms
Changed: public/node-app.js
Reloading daemon after 1.0 seconds...
 * Serving Flask app './src/web_demo/server.py'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

TLS

Live reload is a development feature and not recommended for production.

However there are some instances in which you can only debug behind a TLS server.

To prevent mixed content errors, livereload auto-detects the protocol from the URI. However, we do not ship settings for configuring TLS.

In order to make TLS work, serve the websocket behind a reverse proxy.

If you're using nginx, you would include something like this in your server block:

    location /ws/ {
        proxy_pass http://localhost:51353;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }

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`

  • proxy_uri: URI
Example: `/ws/`

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

How our library makes resource setup better

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

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:

<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&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

<!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&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.

  1"""
  2# html-compose
  3
  4A library for natural HTML composition directly in Python.
  5
  6Focused on fast, flexible, extensible document generation,
  7its goal is to make the web platform fun to work with while using
  8modern browser technologies.
  9
 10## Quick Start
 11
 12All HTML elements from the [living spec](https://html.spec.whatwg.org/multipage/)
 13are available to use with full type hinting:
 14
 15```python
 16from html_compose import a
 17
 18element = a(href="/logout")["Log out"]
 19print(element.render())
 20# <a href="/logout">Log out</a>
 21```
 22
 23The `[]` syntax provides a natural way to define child elements, making the code
 24resemble the HTML structure it represents.
 25
 26Behind the scenes, this is `.base_element.BaseElement.append`, which accepts text,
 27elements, lists, nested lists, and callables. It returns self for chaining.
 28
 29Think of it as:
 30* `()` sets attributes
 31* `[]` adds children
 32
 33Set non-constructor attributes with a dict:
 34
 35```python
 36a({"@click": "alert(1)"}, href="#")["Click me"]
 37```
 38
 39**Security**: The children of HTML elements are always HTML escaped,
 40so XSS directly in the HTML is not possible.
 41
 42JavaScript within HTML attribute values is always escaped.
 43Just don't pass user input into JavaScript attributes.
 44
 45Use `.unsafe_text()` when you need unescaped content.
 46
 47All HTML nodes treat their children as if they contain HTML.
 48This means if you have a `<script>` or `<style>` element or something
 49else that isn't read as HTML, you may need to handle escaping yourself before
 50passing to `.unsafe_text()`.
 51
 52### Imports
 53
 54You can import elements from this module or `html_compose.elements`:
 55
 56- `from html_compose import a, div, span`
 57- `from html_compose.elements import a, div, span`
 58- `import html_compose.elements as el`
 59
 60### Building Documents
 61
 62Use `document_generator` for complete HTML5 documents with optimized resource management:
 63
 64```python
 65from html_compose import p
 66from html_compose.document import document_generator
 67from html_compose.resource import js_import, css_import
 68
 69# Local module with cache-busting
 70admin_script = js_import(
 71    "./static/admin.js",
 72    name="admin",
 73    cache_bust=True,
 74    preload=True
 75)
 76
 77# Remote library
 78alpine = js_import(
 79    'https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js',
 80    defer=True
 81)
 82
 83# CSS with integrity checking and preload
 84bootstrap_css = css_import(
 85    "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css",
 86    preload=True,
 87    hash="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM",
 88    crossorigin="anonymous"
 89)
 90
 91doc: str = document_generator(
 92    title="My App",
 93    css=[bootstrap_css],
 94    js=[admin_script, alpine],
 95    head_extra=[],  # anything you want to add to head
 96    body_content=[
 97        p(class_="container")["Hello, world!"]
 98    ]
 99)
100```
101
102Generates HTML with correct `<link>`, `<script>`, `importmap`, and preload tags
103in the optimal order.
104
105For streaming responses, use `document_streamer` to send the `<head>` early.
106
107### Basic Document API
108
109If you prefer simpler boilerplate generation, `HTML5Document` returns a complete
110document string:
111
112```python
113from html_compose import HTML5Document, p, script, link, meta
114
115doc: str = HTML5Document(
116    "Site Title",
117    lang="en",
118    js=[
119        '/public/bundle.js',
120    ],
121    css=[
122        '/public/style.css',
123    ],
124    head_extra=[
125        meta(name='robots', content='index, follow'),
126    ],
127    body=[p["Hello, world!"]],
128)
129```
130See `html_compose.resource` for advanced resource configuration,
131or view `html_compose.document` for for more on document generation.
132
133
134### Composing Elements
135
136The constructor for an element defines attributes, so if it has none the call
137to the constructor can be skipped like the `p` and `strong` elements below:
138
139```python
140from html_compose import div, strong, a
141
142user = "github wanderer"
143content = div(class_="profile")[
144    p["Welcome, ", strong[user], "!"],
145    a(href="/logout")["Log out"]
146]
147
148print(content.render())
149# <div class="profile"><p>Welcome, <strong>github wanderer</strong>!</p><a href="/logout">Log out</a></div>
150```
151
152## More Features
153
154### Custom Elements
155
156Create custom elements with `CustomElement.create` or `create_element`:
157
158```python
159from html_compose.custom_element import CustomElement
160
161foo = CustomElement.create("foo")
162foo["Hello world"].render()  # <foo>Hello world</foo>
163
164# Or use the shorthand
165from html_compose import create_element
166
167bar = create_element("bar")
168bar()["Hello world"].render()  # <bar>Hello world</bar>
169```
170
171### Type Hints
172
173All elements and attributes are fully type-hinted for IDE support. Your editor
174can complete element names and attributes.
175
176### Flexible Attributes
177
178Attributes support multiple input formats:
179
180```python
181img([img.hint.src("..."), {"@click": "..."}, onmouseleave="..."])
182```
183
184### Extensions
185
186Custom attributes for frameworks can be packaged as reusable modules:
187
188```python
189from pretend_extensions import htmx
190from html_compose import button, div
191
192button([htmx.get('/url'), htmx.target('#result')])
193# <button hx-get="/url" hx-target="#result"></button>
194```
195
196## Command-line Interface
197
198Convert HTML to html-compose syntax—useful when starting from tutorials or templates:
199
200```sh
201html-compose convert {filename or empty for stdin}
202html-compose convert --noimport el  # produces el.div() style references
203html-convert # an alias for html-compose convert
204```
205
206`html-convert` provides access to this tool as shorthand.
207
208# Core Ideas
209We are going to dive into the technicals and core ideas of the library.
210
211.. include:: ../../doc/ideas/01_iterator.md
212.. include:: ../../doc/ideas/02_base_element.md
213.. include:: ../../doc/ideas/03_code_generator.md
214.. include:: ../../doc/ideas/04_attrs.md
215.. include:: ../../doc/ideas/05_livereload.md
216.. include:: ../../doc/ideas/06_resource_imports.md
217"""
218
219from typing import Any, Generator, Iterable, cast
220
221from markupsafe import Markup, escape
222
223from .base_types import Node
224
225
226def escape_text(value) -> Markup:
227    """
228    Escape unsafe text to be inserted into HTML
229
230    Optionally casting to string
231    """
232    if isinstance(value, str):
233        return escape(value)
234    else:
235        return escape(str(value))
236
237
238def unsafe_text(value: str | Markup) -> Markup:
239    """
240    Return input string as Markup
241
242    If input is already markup, it needs no further casting
243    """
244    if isinstance(value, Markup):
245        return value
246
247    return Markup(str(value))
248
249
250def pretty_print(html_str: str, features="html.parser") -> str:
251    """
252    Pretty print HTML.  
253    DO NOT do this for production since it introduces whitespace and may
254    affect your output.
255
256    :param html_str: HTML string to print
257    :param features: BeautifulSoup tree builder to print with
258    :return: Pretty printed HTML string
259    """  # fmt: skip
260    # Production instances probably don't use this
261    # so we lazy load bs4
262    from bs4 import BeautifulSoup  # type: ignore[import-untyped]
263
264    return BeautifulSoup(html_str, features=features).prettify(
265        formatter="html5"
266    )
267
268
269def doctype(dtype: str = "html"):
270    """
271    Return doctype tag
272    """
273    return unsafe_text(f"<!DOCTYPE {dtype}>")
274
275
276# The imports are organized to avoid circular dependencies
277# The import x as x pattern is interpreted by some tools as "public export"
278
279# ruff: noqa: E402
280
281# Library primitives
282from .base_attribute import BaseAttribute as BaseAttribute
283from .base_element import BaseElement as BaseElement
284from .custom_element import CustomElement as CustomElement
285
286
287def stream(html_content: Iterable[Node] | Node) -> Generator[str, Any, None]:
288    """
289    Stream one or more HTML elements as a generator of strings
290
291    :param html_content: An iterable of elements that could be rendered as HTML
292
293    :return: Generator of HTML strings
294    """
295    last = object()
296
297    def generator() -> Generator[str, Any, None]:
298        # We use a dummy element to implement .resolve()
299        dummy = BaseElement(tag="dummy")
300        dummy.append(html_content)
301
302        element_source = dummy.resolve()
303        next(element_source, None)  # Skip dummy start tag
304
305        next_item = next(element_source, last)
306
307        while True:
308            item = cast(str, next_item)
309            next_item = next(element_source, last)
310            if next_item is last:
311                # skip dummy end tag
312                break
313            yield cast(str, item)
314
315    return generator()
316
317
318def render(html_content: Iterable[Node] | Node) -> str:
319    """
320    Render one or more HTML elements into a single string
321
322    :param html_content: An iterable of elements that could be rendered as HTML
323
324    :return: A single HTML string
325    """
326    return "".join(stream(html_content))
327
328
329create_element = CustomElement.create
330# Document features
331from .document import HTML5Document as HTML5Document
332from .document import document_generator as document_generator
333from .document import document_streamer as document_streamer
334
335# Elements
336from .elements import a as a
337from .elements import abbr as abbr
338from .elements import address as address
339from .elements import area as area
340from .elements import article as article
341from .elements import aside as aside
342from .elements import audio as audio
343from .elements import b as b
344from .elements import base as base
345from .elements import bdi as bdi
346from .elements import bdo as bdo
347from .elements import blockquote as blockquote
348from .elements import body as body
349from .elements import br as br
350from .elements import button as button
351from .elements import canvas as canvas
352from .elements import caption as caption
353from .elements import cite as cite
354from .elements import code as code
355from .elements import col as col
356from .elements import colgroup as colgroup
357from .elements import data as data
358from .elements import datalist as datalist
359from .elements import dd as dd
360from .elements import del_ as del_
361from .elements import details as details
362from .elements import dfn as dfn
363from .elements import dialog as dialog
364from .elements import div as div
365from .elements import dl as dl
366from .elements import dt as dt
367from .elements import em as em
368from .elements import embed as embed
369from .elements import fieldset as fieldset
370from .elements import figcaption as figcaption
371from .elements import figure as figure
372from .elements import footer as footer
373from .elements import form as form
374from .elements import h1 as h1
375from .elements import h2 as h2
376from .elements import h3 as h3
377from .elements import h4 as h4
378from .elements import h5 as h5
379from .elements import h6 as h6
380from .elements import head as head
381from .elements import header as header
382from .elements import hgroup as hgroup
383from .elements import hr as hr
384from .elements import html as html
385from .elements import i as i
386from .elements import iframe as iframe
387from .elements import img as img
388from .elements import input as input
389from .elements import ins as ins
390from .elements import kbd as kbd
391from .elements import label as label
392from .elements import legend as legend
393from .elements import li as li
394from .elements import link as link
395from .elements import main as main
396from .elements import map as map
397from .elements import mark as mark
398from .elements import menu as menu
399from .elements import meta as meta
400from .elements import meter as meter
401from .elements import nav as nav
402from .elements import noscript as noscript
403from .elements import object as object
404from .elements import ol as ol
405from .elements import optgroup as optgroup
406from .elements import option as option
407from .elements import output as output
408from .elements import p as p
409from .elements import picture as picture
410from .elements import pre as pre
411from .elements import progress as progress
412from .elements import q as q
413from .elements import rp as rp
414from .elements import rt as rt
415from .elements import ruby as ruby
416from .elements import s as s
417from .elements import samp as samp
418from .elements import script as script
419from .elements import search as search
420from .elements import section as section
421from .elements import select as select
422from .elements import slot as slot
423from .elements import small as small
424from .elements import source as source
425from .elements import span as span
426from .elements import strong as strong
427from .elements import style as style
428from .elements import sub as sub
429from .elements import summary as summary
430from .elements import sup as sup
431from .elements import svg as svg
432from .elements import table as table
433from .elements import tbody as tbody
434from .elements import td as td
435from .elements import template as template
436from .elements import textarea as textarea
437from .elements import tfoot as tfoot
438from .elements import th as th
439from .elements import thead as thead
440from .elements import time as time
441from .elements import title as title
442from .elements import tr as tr
443from .elements import track as track
444from .elements import u as u
445from .elements import ul as ul
446from .elements import var as var
447from .elements import video as video
448from .elements import wbr as wbr
449
450# Resource features
451from .resource import css_import as css_import
452from .resource import font_import_manual as font_import_manual
453from .resource import font_import_provider as font_import_provider
454from .resource import js_import as js_import
def escape_text(value) -> markupsafe.Markup:
227def escape_text(value) -> Markup:
228    """
229    Escape unsafe text to be inserted into HTML
230
231    Optionally casting to string
232    """
233    if isinstance(value, str):
234        return escape(value)
235    else:
236        return escape(str(value))

Escape unsafe text to be inserted into HTML

Optionally casting to string

def unsafe_text(value: str | markupsafe.Markup) -> markupsafe.Markup:
239def unsafe_text(value: str | Markup) -> Markup:
240    """
241    Return input string as Markup
242
243    If input is already markup, it needs no further casting
244    """
245    if isinstance(value, Markup):
246        return value
247
248    return Markup(str(value))

Return input string as Markup

If input is already markup, it needs no further casting

def pretty_print(html_str: str, features='html.parser') -> str:
251def pretty_print(html_str: str, features="html.parser") -> str:
252    """
253    Pretty print HTML.  
254    DO NOT do this for production since it introduces whitespace and may
255    affect your output.
256
257    :param html_str: HTML string to print
258    :param features: BeautifulSoup tree builder to print with
259    :return: Pretty printed HTML string
260    """  # fmt: skip
261    # Production instances probably don't use this
262    # so we lazy load bs4
263    from bs4 import BeautifulSoup  # type: ignore[import-untyped]
264
265    return BeautifulSoup(html_str, features=features).prettify(
266        formatter="html5"
267    )

Pretty print HTML.
DO NOT do this for production since it introduces whitespace and may affect your output.

Parameters
  • html_str: HTML string to print
  • features: BeautifulSoup tree builder to print with
Returns

Pretty printed HTML string

def doctype(dtype: str = 'html'):
270def doctype(dtype: str = "html"):
271    """
272    Return doctype tag
273    """
274    return unsafe_text(f"<!DOCTYPE {dtype}>")

Return doctype tag

def stream( html_content: Union[Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]], NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]) -> Generator[str, Any, NoneType]:
288def stream(html_content: Iterable[Node] | Node) -> Generator[str, Any, None]:
289    """
290    Stream one or more HTML elements as a generator of strings
291
292    :param html_content: An iterable of elements that could be rendered as HTML
293
294    :return: Generator of HTML strings
295    """
296    last = object()
297
298    def generator() -> Generator[str, Any, None]:
299        # We use a dummy element to implement .resolve()
300        dummy = BaseElement(tag="dummy")
301        dummy.append(html_content)
302
303        element_source = dummy.resolve()
304        next(element_source, None)  # Skip dummy start tag
305
306        next_item = next(element_source, last)
307
308        while True:
309            item = cast(str, next_item)
310            next_item = next(element_source, last)
311            if next_item is last:
312                # skip dummy end tag
313                break
314            yield cast(str, item)
315
316    return generator()

Stream one or more HTML elements as a generator of strings

Parameters
  • html_content: An iterable of elements that could be rendered as HTML
Returns

Generator of HTML strings

def render( html_content: Union[Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]], NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]) -> str:
319def render(html_content: Iterable[Node] | Node) -> str:
320    """
321    Render one or more HTML elements into a single string
322
323    :param html_content: An iterable of elements that could be rendered as HTML
324
325    :return: A single HTML string
326    """
327    return "".join(stream(html_content))

Render one or more HTML elements into a single string

Parameters
  • html_content: An iterable of elements that could be rendered as HTML
Returns

A single HTML string

@staticmethod
def create_element( tag: str, void_element: bool = False) -> type[html_compose.custom_element.CustomElement]:
70    @staticmethod
71    def create(tag: str, void_element: bool = False) -> type["CustomElement"]:
72        """
73        Create a new element class with the given tag and void_element flag.
74        This method is a factory for creating new element classes.
75        """
76        return type(
77            safe_name(tag),
78            (CustomElement,),
79            {"tag": tag, "is_void": void_element},
80        )

Create a new element class with the given tag and void_element flag. This method is a factory for creating new element classes.