Skip to content

Instantly share code, notes, and snippets.

@jimbaker
Created May 21, 2022 02:22
Show Gist options
  • Save jimbaker/8ca34e920eb7245a0d1cc093fa8e91f0 to your computer and use it in GitHub Desktop.
Save jimbaker/8ca34e920eb7245a0d1cc093fa8e91f0 to your computer and use it in GitHub Desktop.
tag string implementation for ViewDOM
# 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