Skip to content

Instantly share code, notes, and snippets.

@gvanrossum
Last active May 9, 2022 03:14
Show Gist options
  • Save gvanrossum/a465d31d9402bae2c79e89b2f344c10c to your computer and use it in GitHub Desktop.
Save gvanrossum/a465d31d9402bae2c79e89b2f344c10c to your computer and use it in GitHub Desktop.
# Don't name this file html.py!
from __future__ import annotations
from typing import *
from dataclasses import dataclass
from html import escape
from html.parser import HTMLParser
Thunk = tuple[
Callable[[], Any],
str,
str | None,
str | None,
]
AttrsDict = dict[str, str]
BodyList = list["str | HTMLNode"]
@dataclass
class HTMLNode:
tag: str|None
attrs: AttrsDict
body: BodyList
def __init__(
self,
tag: str|None = None,
attrs: AttrsDict|None = None,
body: BodyList |None = None,
):
self.tag = tag
self.attrs = {}
if attrs:
self.attrs.update(attrs)
self.body = []
if body:
self.body.extend(body)
def __str__(self):
attrlist = []
for key, value in self.attrs.items():
attrlist.append(f' {key}="{escape(str(value))}"')
bodylist = []
for item in self.body:
if isinstance(item, str):
item = escape(item, quote=False)
else:
item = str(item)
bodylist.append(item)
stuff = "".join(bodylist)
if self.tag:
stuff = f"<{self.tag}{''.join(attrlist)}>{stuff}</{self.tag}>"
return stuff
class HTMLBuilder(HTMLParser):
def __init__(self):
self.stack = [HTMLNode()]
super().__init__()
def handle_starttag(self, tag, attrs):
node = HTMLNode(tag, attrs)
self.stack[-1].body.append(node)
self.stack.append(node)
def handle_endtag(self, tag):
if tag != self.stack[-1].tag:
raise RuntimeError(f"unexpected </{tag}>")
self.stack.pop()
def handle_data(self, data: str):
self.stack[-1].body.append(data)
# This is the actual 'tag' function: html"<body>blah</body>""
def html(*args: str | Thunk) -> HTMLTree:
builder = HTMLBuilder()
for arg in args:
if isinstance(arg, str):
builder.feed(arg) # TODO: interpret \n, \x, \u, etc.
else:
getvalue, raw, conv, spec = arg
value = getvalue()
match conv:
case 'r': value = repr(value)
case 's': value = str(value)
case 'a': value = ascii(value)
case None: pass
case _: raise ValueError(f"Bad conversion: {conv!r}")
if spec is not None:
value = format(value, spec)
if isinstance(value, HTMLNode):
builder.feed(str(value))
elif isinstance(value, list):
for item in value:
if isinstance(item, HTMLNode):
builder.feed(str(item))
else:
builder.feed(escape(str(item)))
else:
builder.feed(escape(str(value)))
root = builder.stack[0]
if not root.tag and not root.attrs:
stuff = root.body[:]
while stuff and isinstance(stuff[0], str) and stuff[0].isspace():
del stuff[0]
while stuff and isinstance(stuff[-1], str) and stuff[-1].isspace():
del stuff[-1]
if len(stuff) == 1:
return stuff[0]
return stuff
return root
x = HTMLNode("foo", {"x": 1, "y": "2"}, ["hohoho"])
a = html"""
<html>
<body>
foo
{x}
bar
{x!s}
baz
{x!r}
</body>
</html>
"""
print(a)
print()
s = '"'
b = html"""
<html>
<body attr=blah" yo={1}>
{[html"<div class=c{i}>haha{i}</div> " for i in range(3)]}
</body>
</html>
"""
print(b)
<html>
<body>
foo
<foo x="1" y="2">hohoho</foo>
bar
&lt;foo x="1" y="2"&gt;hohoho&lt;/foo&gt;
baz
HTMLNode(tag='foo', attrs={'x': 1, 'y': '2'}, body=['hohoho'])
</body>
</html>
<html>
<body attr="blah&quot;" yo="1">
<div class="c0">haha0</div><div class="c1">haha1</div><div class="c2">haha2</div>
</body>
</html>
@gvanrossum
Copy link
Author

Note that an interpolation may now be a list of HTMLNodes that are rendered as such.

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