-
-
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<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 |
So this snippet could be a potential issue for users, but maybe not. First, note that changing the scope doesn't actually matter:
b = html
"""
<html>
<body attr=blah" yo={1}>
{[html"<div class=c{i}>haha{i}</div> " for i in range(3)]}
{TodoList('High: ', ['Get milk', 'Change tires'])}
</body>
</html>
print(b)
"""
It's actually legal code in Python without this branch. b
is assigned to html
, which is a function. Then you have some string """..."""
. Then you print b
. Everything as written, but obviously not what you expected!
Tag-strings, at least currently in this preliminary stage, work like regular prefixes - there's no space between the tag and the quotes. Note that we solve it just like other usage:
>>> f "foo"
File "<stdin>", line 1
f "foo"
^^^^^
SyntaxError: invalid syntax
and in this branch
>>> html "foo"
File "<stdin>", line 1
html "foo"
^
SyntaxError: cannot have space between tag and string
In practice, maybe this is not such a problem:
>>> def f(): return 42
...
>>> f
<function f at 0x7fbb3e86e3b0>
>>> "xyz"
'xyz'
Or this code, let's call it foo.py
:
a = 42
b = f
"""
Some stuff {a} goes here
"""
Then run it:
~$ python foo.py
Traceback (most recent call last):
File "/home/jimbaker/test/foo.py", line 3, in <module>
b = f
NameError: name 'f' is not defined
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.
If I use your example from the thread, this works:
But if I move it into a main block, or a
render()
function (change the scope), the print returns the html function: