Created
May 21, 2022 00:52
-
-
Save jimbaker/80da6a441a4f488bb320cf48457d9192 to your computer and use it in GitHub Desktop.
tag string implementation for constructing IDOM's `VdomDict`
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 idom.core.vdom import vdom, VdomDict | |
# 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}', {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(vdom, *args) | |
def useit(): | |
for i in range(3): | |
for j in range(3): | |
print(html'<body attr1=47><div>{i} along with {j}</div></body>') | |
if __name__ == '__main__': | |
useit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment