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

If I use your example from the thread, this works:

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)

But if I move it into a main block, or a render() function (change the scope), the print returns the html function:

if __name__ == '__main__':
    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)

# Output: <function html at 0x1008fc540>

@jimbaker
Copy link
Author

jimbaker commented May 8, 2022

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

@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