Skip to content

Instantly share code, notes, and snippets.

@jviide
Last active March 21, 2020 19:06
Show Gist options
  • Save jviide/e06c0c88f68cb7991377a5ce70d9d8df to your computer and use it in GitHub Desktop.
Save jviide/e06c0c88f68cb7991377a5ce70d9d8df to your computer and use it in GitHub Desktop.
Proof-of-concept: Generating HTML with htm.py

Proof-of-concept: Generating HTML with htm.py

A demo of generating HTML with just htm.py and the Python 3 standard library. This idea could, maybe, be combined with this gist.

h.py is the main "library" that implements html for generating the DOM and render for rendering it.

example.py uses h.py and illustrates some general patterns, such as:

  • functional components (see functional_component)
  • generator components (see generator_component)
  • boolean props (see editable in the output)
  • components only getting the props they're asking for (via relaxed_call in h.py)
  • fragments and nested child lists (via flatten in h.py)
  • conditional rendering (True/False/None get ignored - see footer_message)

To run, first install htm.py with pip3 install htm and then run the command python3 example.py.

Note that this is just a PoC. For example tags and prop names are not validated properly to not contain spaces etc.

from h import html, render
def functional_component(children, header="Functional components!"):
return html("""
<h2>{header}</h2>
{children}
""")
def generator_component():
for text in ["foo", "bar"]:
yield html("<li>{text}</li>")
footer_message = None
page = html("""
<div>
<h1>Hello Python</h1>
<p>Now you can write HTML in Python!</p>
<p editable>With boolean props!</p>
<!-- and functional components -->
<{functional_component}>
<!-- and generators! -->
<{generator_component} />
<//>
</div>
<!-- This gets ignored -->
{footer_message and html("<footer>{footer_message}<//>")}
""")
print(render(page))
import threading
from htm import htm
from html import escape as _escape
from collections import namedtuple, ChainMap
from collections.abc import Iterable, ByteString
from inspect import signature, Parameter
H = namedtuple("H", ["tag", "props", "children"])
html = htm(H)
def flatten(value):
if isinstance(value, Iterable) and not isinstance(value, (H, str, ByteString)):
for item in value:
yield from flatten(item)
else:
yield value
def relaxed_call(callable_, **kwargs):
# Hackety-hack-hack warning!
# Call callable_ with the given keyword arguments, but
# only those that callable_ expects - ignore others.
sig = signature(callable_)
parameters = sig.parameters
if not any(p.kind == p.VAR_KEYWORD for p in parameters.values()):
extra_key = "_"
while extra_key in parameters:
extra_key += "_"
sig = sig.replace(
parameters=[*parameters.values(), Parameter(extra_key, Parameter.VAR_KEYWORD)])
kwargs = dict(sig.bind(**kwargs).arguments)
kwargs.pop(extra_key, None)
return callable_(**kwargs)
def render(value, **kwargs):
return "".join(render_gen(Context(value, **kwargs)))
def render_gen(value):
for item in flatten(value):
if isinstance(item, H):
tag, props, children = item
if callable(tag):
yield from render_gen(relaxed_call(tag, children=children, **props))
continue
yield f"<{escape(tag)}"
if props:
yield f" {' '.join(encode_prop(k, v) for (k, v) in props.items())}"
if children:
yield ">"
yield from render_gen(children)
yield f'</{escape(tag)}>'
else:
yield f'/>'
elif item not in (True, False, None):
yield escape(item)
def escape(value):
return _escape(str(value))
def encode_prop(k, v):
if v is True:
return escape(k)
return f'{escape(k)}="{escape(v)}"'
_local = threading.local()
def Context(children=None, **kwargs):
context = getattr(_local, "context", ChainMap())
try:
_local.context = context.new_child(kwargs)
yield children
finally:
_local.context = context
def use_context(key, default=None):
context = getattr(_local, "context", ChainMap())
return context.get(key, default)
@jviide
Copy link
Author

jviide commented Mar 21, 2020

def Wired(container, children=None):
    return html("<{Context} wired={container}>{children}<//>")

def use_wired(*args, **keys):
    container = use_context("wired")
    return container.get(*args, **keys)

def App():
    the_greeter = use_wired(Greeter)
    greeting = the_greeter()
    return html("<div>{greeting}</div>")

container = ...

print(render(html("""
    <Wired container={container}>
        <App />
    <//>
""")))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment