-
-
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: