Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

HTPY Integration #1282

dickermoshe started this conversation in Ideas
Feb 23, 2025 · 1 comments · 4 replies
Discussion options

HTPY has already solved many of the UX issues when working with html-in-python.

For instance

  1. VScode extension for converting html to python. https://htpy.dev/html2htpy/
  2. Shorthand for class and id using css selectors. https://htpy.dev/usage/#idclass-shorthand
  3. Use Booleans to define class names https://htpy.dev/usage/#conditionally-mixing-css-classes
  4. Context decorators https://htpy.dev/usage/#passing-data-with-context
  5. XSS stripping of the html
  6. Inject markdown https://htpy.dev/usage/#injecting-markup

Because htmy is more focused on this one narrow issue and reactpy is trying to solve a much broader one, would it make sense to migrate/integrate this approach which seems to have a more narrow focus on doing this one task very well?

Thanks!

You must be logged in to vote

Replies: 1 comment 4 replies

Comment options

I have a feeling there would be some awkward hacks needed to get htpy to create ReactJS compatible output.

Do you think this could be prototyped external to ReactPy?

You must be logged in to vote
4 replies
Comment options

Integrating this with htpy is not necessary. We really could just wrap the existing elements with a better API.

Migrating the html.h1() syntax would probably b a really big breaking change that you wouldn't want though.

Comment options

The below would allow you to use htpy syntax with reactpy

Details
from __future__ import annotations
from uuid import uuid4
from reactpy import html as rhtml, component
from reactpy.core.types import VdomDictConstructor, VdomDict, ComponentType, Key
import typing as t
import keyword
from collections.abc import Iterable, Mapping
from markupsafe import escape as _escape
def _force_escape(value: t.Any) -> str:
 return str(_escape(str(value)))
# Inspired by https://www.npmjs.com/package/classnames
def _class_names(items: t.Any) -> t.Any:
 if isinstance(items, str):
 return _force_escape(items)
 if isinstance(items, dict) or not isinstance(items, Iterable):
 items = [items]
 result = list(_class_names_for_items(items))
 if not result:
 return False
 return " ".join(_force_escape(class_name) for class_name in result)
def _class_names_for_items(items: t.Any) -> t.Any:
 for item in items:
 if isinstance(item, dict):
 for k, v in item.items(): # pyright: ignore [reportUnknownVariableType]
 if v:
 yield k
 else:
 if item:
 yield item
def _id_class_names_from_css_str(x: t.Any) -> Mapping[str, Attribute]:
 if not isinstance(x, str):
 raise TypeError(f"id/class strings must be str. got {x}")
 if "#" in x and "." in x and x.find("#") > x.find("."):
 raise ValueError("id (#) must be specified before classes (.)")
 if x[0] not in ".#":
 raise ValueError("id/class strings must start with # or .")
 parts = x.split(".")
 ids = [part.removeprefix("#") for part in parts if part.startswith("#")]
 classes = [part for part in parts if not part.startswith("#") if part]
 assert len(ids) in (0, 1)
 result: dict[str, Attribute] = {}
 if ids:
 result["id"] = ids[0]
 if classes:
 result["class"] = " ".join(classes)
 return result
def _python_to_html_name(name: str) -> str:
 # Make _hyperscript (https://hyperscript.org/) work smoothly
 if name == "_":
 return "_"
 html_name = name
 name_without_underscore_suffix = name.removesuffix("_")
 if keyword.iskeyword(name_without_underscore_suffix):
 html_name = name_without_underscore_suffix
 html_name = html_name.replace("_", "-")
 return html_name
def _generate_attrs(raw_attrs: Mapping[str, Attribute]) -> Iterable[tuple[str, Attribute]]:
 for key, value in raw_attrs.items():
 if not isinstance(key, str): # pyright: ignore [reportUnnecessaryIsInstance]
 raise TypeError("Attribute key must be a string")
 if value is False or value is None:
 continue
 if key == "class":
 if result := _class_names(value):
 yield ("class", result)
 elif value is True:
 yield _force_escape(key), True
 else:
 if not isinstance(value, str | int):
 raise TypeError(f"Attribute value must be a string or an integer , got {value!r}")
 yield _force_escape(key), _force_escape(value)
VdomChild: t.TypeAlias = "Element | ComponentType | VdomDict | str | None | t.Any"
VdomChildren: t.TypeAlias = "t.Sequence[VdomChild] | VdomChild"
"""Describes a series of :class:`VdomChild` elements"""
class Element(ComponentType):
 def __init__(
 self,
 name: VdomDictConstructor,
 *,
 allow_children: bool = True,
 attrs: Mapping[str, Attribute] | None = None,
 children: t.Sequence[VdomChild] | None = None,
 ):
 self._buildTag = name
 self._allow_children = allow_children
 self._attrs: Mapping[str, Attribute] = attrs or {}
 self._children: t.Sequence[VdomChild] = children or []
 self.type = uuid4()
 # Abstract methods for ComponentType
 def render(self) -> VdomDict:
 built_children = []
 for child in self._children:
 if isinstance(child, Element):
 built_children.append(child.render())
 else:
 built_children.append(child)
 return self._buildTag(self._attrs, *built_children)
 # Abstract methods for ComponentType
 @property
 def key(self) -> Key | None:
 if self._attrs:
 key = self._attrs.get("key")
 if isinstance(key, str) or isinstance(key, int) or key is None:
 return key
 raise TypeError(f"key must be a string, an integer, or None, got {key!r}")
 return None
 # Abstract methods for ComponentType
 @key.setter
 def key(self, value) -> None: # type: ignore
 if self._attrs:
 self._attrs = {**self._attrs, "key": value}
 def __getitem__(self, children: VdomChildren) -> t.Self:
 if not self._allow_children:
 raise TypeError(f"{self.__class__.__name__} does not support children")
 if not isinstance(children, t.Sequence):
 children = [children]
 return self.__class__(
 self._buildTag,
 allow_children=self._allow_children,
 attrs=self._attrs,
 children=children,
 )
 @t.overload
 def __call__(
 self: ElementSelf, id_class: str, attrs: Mapping[str, Attribute], **kwargs: Attribute
 ) -> ElementSelf: ...
 @t.overload
 def __call__(self: ElementSelf, id_class: str = "", **kwargs: Attribute) -> ElementSelf: ...
 @t.overload
 def __call__(self: ElementSelf, attrs: Mapping[str, Attribute], **kwargs: Attribute) -> ElementSelf: ...
 @t.overload
 def __call__(self: ElementSelf, **kwargs: Attribute) -> ElementSelf: ...
 def __call__(self: ElementSelf, *args: t.Any, **kwargs: t.Any) -> ElementSelf:
 id_class = ""
 attrs: Mapping[str, Attribute] = {}
 if len(args) == 1:
 if isinstance(args[0], str):
 # element(".foo")
 id_class = args[0]
 attrs = {}
 else:
 # element({"foo": "bar"})
 id_class = ""
 attrs = args[0]
 elif len(args) == 2:
 # element(".foo", {"bar": "baz"})
 id_class, attrs = args
 attrs = {
 **(_id_class_names_from_css_str(id_class) if id_class else {}),
 **attrs,
 **{_python_to_html_name(k): v for k, v in kwargs.items()},
 }
 attrs = {k: v for k, v in _generate_attrs(attrs)}
 # Rename class to class_name
 if "class" in attrs:
 attrs["class_name"] = attrs.pop("class")
 # Rename for to html_for
 if "for" in attrs:
 attrs["html_for"] = attrs.pop("for")
 return self.__class__(
 self._buildTag,
 allow_children=self._allow_children,
 attrs=attrs,
 children=self._children,
 )
# Document metadata
base = Element(rhtml.base)
head = Element(rhtml.head)
link = Element(rhtml.link)
meta = Element(rhtml.meta)
style = Element(rhtml.style)
title = Element(rhtml.title)
# Content sectioning
address = Element(rhtml.address)
article = Element(rhtml.article)
aside = Element(rhtml.aside)
footer = Element(rhtml.footer)
header = Element(rhtml.header)
h1 = Element(rhtml.h1)
h2 = Element(rhtml.h2)
h3 = Element(rhtml.h3)
h4 = Element(rhtml.h4)
h5 = Element(rhtml.h5)
h6 = Element(rhtml.h6)
main = Element(rhtml.main)
nav = Element(rhtml.nav)
section = Element(rhtml.section)
# Text content
blockquote = Element(rhtml.blockquote)
dd = Element(rhtml.dd)
div = Element(rhtml.div)
dl = Element(rhtml.dl)
dt = Element(rhtml.dt)
figcaption = Element(rhtml.figcaption)
figure = Element(rhtml.figure)
hr = Element(rhtml.hr, allow_children=False)
li = Element(rhtml.li)
ol = Element(rhtml.ol)
p = Element(rhtml.p)
pre = Element(rhtml.pre)
ul = Element(rhtml.ul)
# Inline text semantics
a = Element(rhtml.a)
abbr = Element(rhtml.abbr)
b = Element(rhtml.b)
bdi = Element(rhtml.bdi)
bdo = Element(rhtml.bdo)
br = Element(rhtml.br, allow_children=False)
cite = Element(rhtml.cite)
code = Element(rhtml.code)
data = Element(rhtml.data)
em = Element(rhtml.em)
i = Element(rhtml.i)
kbd = Element(rhtml.kbd)
mark = Element(rhtml.mark)
q = Element(rhtml.q)
rp = Element(rhtml.rp)
rt = Element(rhtml.rt)
ruby = Element(rhtml.ruby)
s = Element(rhtml.s)
samp = Element(rhtml.samp)
small = Element(rhtml.small)
span = Element(rhtml.span)
strong = Element(rhtml.strong)
sub = Element(rhtml.sub)
sup = Element(rhtml.sup)
time = Element(rhtml.time)
u = Element(rhtml.u)
var = Element(rhtml.var)
wbr = Element(rhtml.wbr)
# Image and video
area = Element(rhtml.area, allow_children=False)
audio = Element(rhtml.audio)
img = Element(rhtml.img, allow_children=False)
map = Element(rhtml.map)
track = Element(rhtml.track)
video = Element(rhtml.video)
# Embedded content
embed = Element(rhtml.embed, allow_children=False)
iframe = Element(rhtml.iframe, allow_children=False)
object = Element(rhtml.object)
param = Element(rhtml.param)
picture = Element(rhtml.picture)
portal = Element(rhtml.portal, allow_children=False)
source = Element(rhtml.source, allow_children=False)
# SVG and MathML
svg = Element(rhtml.svg)
math = Element(rhtml.math)
# Scripting
canvas = Element(rhtml.canvas)
noscript = Element(rhtml.noscript)
input = Element(rhtml.input)
ElementSelf = t.TypeVar("ElementSelf", bound="Element")
_ClassNamesDict: t.TypeAlias = dict[str, bool]
_ClassNames: t.TypeAlias = Iterable[str | None | bool | _ClassNamesDict] | _ClassNamesDict
Attribute: t.TypeAlias = None | bool | str | int | _ClassNames

Usage

@component
def hello_world():
 q = div(".p-8")[input(
 "#large-input.block.w-full.p-4.text-gray-900.border.border-gray-300.rounded-lg.bg-gray-50.text-base.focus:ring-blue-500.focus:border-blue-500.dark:bg-gray-700.dark:border-gray-600.dark:placeholder-gray-400.dark:text-white.dark:focus:ring-blue-500.dark:focus:border-blue-500",
 type="text",
 placeholder="Pride of the Farm Milk",
 key="hi",
 )]
Comment options

I do have a similar syntax half-prototyped that I was thinking of releasing as a external package.

My prototype's user API was intended to look like this:

reactpy.html(class_name="p-8")[
 child_1(...),
 child_2(...),
] 

After ReactPy v2 is released in the next 2-3 months (currently blocked on documentation re-write), it will be substantially easier to develop due to VDOM construction changes. But I'm not sure when/if I'd have time to create this, so ideally someone else would take up the mantle.

Comment options

I'll take a whack at it then. I also really want to work on a cli/web app that converts html to reactpy code.
Copy-Pasting html from LLM is not really possible now.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Ideas
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /