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 |
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
So this snippet could be a potential issue for users, but maybe not. First, note that changing the scope doesn't actually matter:
It's actually legal code in Python without this branch.
b
is assigned tohtml
, which is a function. Then you have some string"""..."""
. Then you printb
. 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:
and in this branch
In practice, maybe this is not such a problem:
Or this code, let's call it
foo.py
:Then run it: