Created
May 6, 2022 23:17
-
-
Save jimbaker/2ec6df593ea456c77c53327c1e79a18f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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<table>s</table></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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.