-
-
Notifications
You must be signed in to change notification settings - Fork 328
HTPY Integration #1282
-
HTPY has already solved many of the UX issues when working with html-in-python.
For instance
- VScode extension for converting html to python. https://htpy.dev/html2htpy/
- Shorthand for class and id using css selectors. https://htpy.dev/usage/#idclass-shorthand
- Use Booleans to define class names https://htpy.dev/usage/#conditionally-mixing-css-classes
- Context decorators https://htpy.dev/usage/#passing-data-with-context
- XSS stripping of the html
- 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!
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 1 comment 4 replies
-
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?
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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", )]
Beta Was this translation helpful? Give feedback.
All reactions
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
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!
Beta Was this translation helpful? Give feedback.