Skip to content

Instantly share code, notes, and snippets.

@jimbaker
Created May 6, 2022 23:17
Show Gist options
  • Save jimbaker/2ec6df593ea456c77c53327c1e79a18f to your computer and use it in GitHub Desktop.
Save jimbaker/2ec6df593ea456c77c53327c1e79a18f to your computer and use it in GitHub Desktop.
# Extracted from @gvanrossum's gist https://gist.github.com/gvanrossum/a465d31d9402bae2c79e89b2f344c10c
# Demonstrates tag-string functionality, as tracked in https://jimbaker/tagstr
# Requires an implementating branch, as in https://github.com/jimbaker/tagstr/issues/1
# Sample usage:
# from htmltag import html
#
# >>> user = "Bobby<table>s</table>"
# >>> print(html"<div>Hello {user}</div>")
# <div>Hello Bobby&lt;table&gt;s&lt;/table&gt;</div>
# 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) -> HTMLNode:
builder = HTMLBuilder()
for arg in args:
if isinstance(arg, str):
builder.feed(arg)
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
@pauleveritt
Copy link

My deepest apologies, I didn't even notice that my editor helpfully re-formatted to split html from the string. Geez.

Ok, on to looking at hooking this into htm.py and possibly making a video.

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