Created
May 21, 2022 02:22
-
-
Save jimbaker/8ca34e920eb7245a0d1cc093fa8e91f0 to your computer and use it in GitHub Desktop.
tag string implementation for ViewDOM
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
# Don't name this file html.py | |
from __future__ import annotations | |
from functools import cache | |
from types import CodeType | |
from typing import * | |
from html.parser import HTMLParser | |
from viewdom import VDOMNode, render | |
# getvalue, raw, conv, formatspec | |
Thunk = tuple[ | |
Callable[[], Any], | |
str, | |
str | None, | |
str | None, | |
] | |
class VdomCodeBuilder(HTMLParser): | |
"""Given HTML input with interpolation, builds source code to construct equivalent vdom""" | |
def __init__(self): | |
self.lines = ['def compiled(vdom, /, *args):', ' return \\'] | |
self.tag_stack = [] | |
super().__init__() | |
def indent(self) -> str: | |
return ' ' * (len(self.tag_stack) + 2) | |
@property | |
def code(self) -> str: | |
return '\n'.join(self.lines) | |
def handle_starttag(self, tag, attrs): | |
self.lines.append(f"{self.indent()}vdom('{tag}', {dict(attrs)!r}, [") | |
self.tag_stack.append(tag) | |
def handle_endtag(self, tag): | |
if tag != self.tag_stack[-1]: | |
raise RuntimeError(f"unexpected </{tag}>") | |
self.tag_stack.pop() | |
self.lines.append(f'{self.indent()}]){"," if self.tag_stack else ""}') | |
def handle_data(self, data: str): | |
# At the very least the first empty line needs to be removed, as might | |
# be seen in | |
# html""" | |
# <tag>..</tag> | |
# """ | |
# (given that our codegen is so rudimentary!) | |
# Arguably other blank strings should be removed as well, and this | |
# stripping results in having output equivalent to standard vdom | |
# construction. | |
if not data.strip(): | |
return | |
self.lines.append(f'{self.indent()}{data!r},') | |
def add_interpolation(self, i: int): | |
# Call getvalue for the thunk at the i-th position in args. This | |
# interpolation could optionally also process formatspec, conversion. | |
self.lines.append(f'{self.indent()}args[{i}][0](), ') | |
@cache | |
def make_compiled_template(*args: str | CodeType) -> Callable: | |
print(f'Making compiled template {hash(args)}...') | |
builder = VdomCodeBuilder() | |
for i, arg in enumerate(args): | |
if isinstance(arg, str): | |
builder.feed(arg) | |
else: | |
# NOTE: Use a code object to represent the function interpolation. | |
# However, we are just passing in the argument position, so it's | |
# reasonable. | |
builder.add_interpolation(i) | |
print(builder.code) | |
code_obj = compile(builder.code, '<string>', 'exec') | |
captured = {} | |
exec(code_obj, captured) | |
return captured['compiled'] | |
# The "lambda wrappers" function object will change at each usage of the call | |
# site. Let's use the underlying code object instead as part of the key to | |
# construct the compiled function so it can be memoized. This approach is | |
# correct, since we will call getvalue in the thunk in the interpolation. | |
def immutable_bits(*args: str | Thunk) -> Tuple(str | CodeType): | |
bits = [] | |
for arg in args: | |
if isinstance(arg, str): | |
bits.append(arg) | |
else: | |
bits.append((arg[0].__code__,)) | |
return tuple(bits) | |
# This is the actual 'tag' function: html"<body>blah</body>" | |
def html(*args: str | Thunk) -> VdomDict: | |
compiled = make_compiled_template(*immutable_bits(*args)) | |
return compiled(VDOMNode, *args) | |
def useit(): | |
for i in range(3): | |
for j in range(3): | |
node = html'<body attr1=47><div>{i} along with {j}</div></body>' | |
print(node) | |
print(render(node)) | |
if __name__ == '__main__': | |
useit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment