reflex/reflex/components/markdown/markdown.py
Khaleel Al-Adhami a5c73ad8e5
Use old serializer system in LiteralVar (#3875)
* use serializer system

* add checks for unsupported operands

* and and or are now supported

* format

* remove unnecessary call to JSON

* put base before rest

* fix failing testcase

* add hinting to get static analysis to complain

* damn

* big changes

* get typeguard from extensions

* please darglint

* dangit darglint

* remove one from vars

* add without data and use it in plotly

* DARGLINT

* change format for special props

* add pyi

* delete instances of Var.create

* modify client state to work

* fixed so much

* remove every Var.create

* delete all basevar stuff

* checkpoint

* fix pyi

* get older python to work

* dangit darglint

* add simple fix to last failing testcase

* remove var name unwrapped and put client state on immutable var

* fix older python

* fox event issues

* change forms pyi

* make test less strict

* use rx state directly

* add typeignore to page_id

* implement foreach

* delete .web states folder silly

* update reflex chakra

* fix issue when on mount or on unmount is not set

* nuke Var

* run pyi

* import immutablevar in critical location

* delete unwrap vars

* bring back array ref

* fix style props in app

* /health endpoint for K8 Liveness and Readiness probes (#3855)

* Added API Endpoint

* Added API Endpoint

* Added Unit Tests

* Added Unit Tests

* main

* Apply suggestions from Code Review

* Fix Ruff Formatting

* Update Socket Events

* Async Functions

* Update find_replace (#3886)

* [REF-3592]Promote `rx.progress` from radix themes (#3878)

* Promote `rx.progress` from radix themes

* fix pyi

* add warning when accessing `rx._x.progress`

* Use correct flexgen backend URL (#3891)

* Remove demo template (#3888)

* gitignore .web (#3885)

* update overflowY in AUTO_HEIGHT_JS from hidden to scroll (#3882)

* Retain mutability inside `async with self` block (#3884)

When emitting a state update, restore `_self_mutable` to the value it had
previously so that `yield` in the middle of `async with self` does not result
in an immutable StateProxy.

Fix #3869

* Include child imports in markdown component_map (#3883)

If a component in the markdown component_map contains children components, use
`_get_all_imports` to recursively enumerate them.

Fix #3880

* [REF-3570] Remove deprecated REDIS_URL syntax (#3892)

* mixin computed vars should only be applied to highest level state (#3833)

* improve state hierarchy validation, drop old testing special case (#3894)

* fix var dependency dicts (#3842)

* Adding array to array pluck operation. (#3868)

* fix initial state without cv fallback (#3670)

* add fragment to foreach (#3877)

* Update docker-example (#3324)

* /health endpoint for K8 Liveness and Readiness probes (#3855)

* Added API Endpoint

* Added API Endpoint

* Added Unit Tests

* Added Unit Tests

* main

* Apply suggestions from Code Review

* Fix Ruff Formatting

* Update Socket Events

* Async Functions

* /health endpoint for K8 Liveness and Readiness probes (#3855)

* Added API Endpoint

* Added API Endpoint

* Added Unit Tests

* Added Unit Tests

* main

* Apply suggestions from Code Review

* Fix Ruff Formatting

* Update Socket Events

* Async Functions

* Merge branch 'main' into use-old-serializer-in-literalvar

* [REF-3570] Remove deprecated REDIS_URL syntax (#3892)

* /health endpoint for K8 Liveness and Readiness probes (#3855)

* Added API Endpoint

* Added API Endpoint

* Added Unit Tests

* Added Unit Tests

* main

* Apply suggestions from Code Review

* Fix Ruff Formatting

* Update Socket Events

* Async Functions

* [REF-3570] Remove deprecated REDIS_URL syntax (#3892)

* remove extra var

Co-authored-by: Masen Furer <m_github@0x26.net>

* resolve typo

* write better doc for var.create

* return var value when we know it's literal var

* fix unit test

* less bloat for ToOperations

* simplify ImmutableComputedVar.__get__ (#3902)

* simplify ImmutableComputedVar.__get__

* ruff it

---------

Co-authored-by: Samarth Bhadane <samarthbhadane119@gmail.com>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>
Co-authored-by: Masen Furer <m_github@0x26.net>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Vishnu Deva <vishnu.deva12@gmail.com>
Co-authored-by: abulvenz <a.eismann@senbax.de>
2024-09-10 11:43:37 -07:00

308 lines
10 KiB
Python

"""Markdown component."""
from __future__ import annotations
import textwrap
from functools import lru_cache
from hashlib import md5
from typing import Any, Callable, Dict, Union
from reflex.components.component import Component, CustomComponent
from reflex.components.radix.themes.layout.list import (
ListItem,
OrderedList,
UnorderedList,
)
from reflex.components.radix.themes.typography.heading import Heading
from reflex.components.radix.themes.typography.link import Link
from reflex.components.radix.themes.typography.text import Text
from reflex.components.tags.tag import Tag
from reflex.ivars.base import ImmutableVar, LiteralVar
from reflex.utils import types
from reflex.utils.imports import ImportDict, ImportVar
# Special vars used in the component map.
_CHILDREN = ImmutableVar.create_safe("children")
_PROPS = ImmutableVar.create_safe("...props")
_PROPS_IN_TAG = ImmutableVar.create_safe("{...props}")
_MOCK_ARG = ImmutableVar.create_safe("")
# Special remark plugins.
_REMARK_MATH = ImmutableVar.create_safe("remarkMath")
_REMARK_GFM = ImmutableVar.create_safe("remarkGfm")
_REMARK_UNWRAP_IMAGES = ImmutableVar.create_safe("remarkUnwrapImages")
_REMARK_PLUGINS = LiteralVar.create([_REMARK_MATH, _REMARK_GFM, _REMARK_UNWRAP_IMAGES])
# Special rehype plugins.
_REHYPE_KATEX = ImmutableVar.create_safe("rehypeKatex")
_REHYPE_RAW = ImmutableVar.create_safe("rehypeRaw")
_REHYPE_PLUGINS = LiteralVar.create([_REHYPE_KATEX, _REHYPE_RAW])
# These tags do NOT get props passed to them
NO_PROPS_TAGS = ("ul", "ol", "li")
# Component Mapping
@lru_cache
def get_base_component_map() -> dict[str, Callable]:
"""Get the base component map.
Returns:
The base component map.
"""
from reflex.components.datadisplay.code import CodeBlock
from reflex.components.radix.themes.typography.code import Code
return {
"h1": lambda value: Heading.create(value, as_="h1", size="6", margin_y="0.5em"),
"h2": lambda value: Heading.create(value, as_="h2", size="5", margin_y="0.5em"),
"h3": lambda value: Heading.create(value, as_="h3", size="4", margin_y="0.5em"),
"h4": lambda value: Heading.create(value, as_="h4", size="3", margin_y="0.5em"),
"h5": lambda value: Heading.create(value, as_="h5", size="2", margin_y="0.5em"),
"h6": lambda value: Heading.create(value, as_="h6", size="1", margin_y="0.5em"),
"p": lambda value: Text.create(value, margin_y="1em"),
"ul": lambda value: UnorderedList.create(value, margin_y="1em"), # type: ignore
"ol": lambda value: OrderedList.create(value, margin_y="1em"), # type: ignore
"li": lambda value: ListItem.create(value, margin_y="0.5em"),
"a": lambda value: Link.create(value),
"code": lambda value: Code.create(value),
"codeblock": lambda value, **props: CodeBlock.create(
value, margin_y="1em", wrap_long_lines=True, **props
),
}
class Markdown(Component):
"""A markdown component."""
library = "react-markdown@8.0.7"
tag = "ReactMarkdown"
is_default = True
# The component map from a tag to a lambda that creates a component.
component_map: Dict[str, Any] = {}
# The hash of the component map, generated at create() time.
component_map_hash: str = ""
@classmethod
def create(cls, *children, **props) -> Component:
"""Create a markdown component.
Args:
*children: The children of the component.
**props: The properties of the component.
Returns:
The markdown component.
"""
assert (
len(children) == 1
and types._isinstance(children[0], Union[str, ImmutableVar])
), "Markdown component must have exactly one child containing the markdown source."
# Update the base component map with the custom component map.
component_map = {**get_base_component_map(), **props.pop("component_map", {})}
# Get the markdown source.
src = children[0]
# Dedent the source.
if isinstance(src, str):
src = textwrap.dedent(src)
# Create the component.
return super().create(
src,
component_map=component_map,
component_map_hash=cls._component_map_hash(component_map),
**props,
)
def _get_all_custom_components(
self, seen: set[str] | None = None
) -> set[CustomComponent]:
"""Get all the custom components used by the component.
Args:
seen: The tags of the components that have already been seen.
Returns:
The set of custom components.
"""
custom_components = super()._get_all_custom_components(seen=seen)
# Get the custom components for each tag.
for component in self.component_map.values():
custom_components |= component(_MOCK_ARG)._get_all_custom_components(
seen=seen
)
return custom_components
def add_imports(self) -> ImportDict | list[ImportDict]:
"""Add imports for the markdown component.
Returns:
The imports for the markdown component.
"""
from reflex.components.datadisplay.code import CodeBlock
from reflex.components.radix.themes.typography.code import Code
return [
{
"": "katex/dist/katex.min.css",
"remark-math@5.1.1": ImportVar(
tag=_REMARK_MATH._var_name, is_default=True
),
"remark-gfm@3.0.1": ImportVar(
tag=_REMARK_GFM._var_name, is_default=True
),
"remark-unwrap-images@4.0.0": ImportVar(
tag=_REMARK_UNWRAP_IMAGES._var_name, is_default=True
),
"rehype-katex@6.0.3": ImportVar(
tag=_REHYPE_KATEX._var_name, is_default=True
),
"rehype-raw@6.1.1": ImportVar(
tag=_REHYPE_RAW._var_name, is_default=True
),
},
*[
component(_MOCK_ARG)._get_all_imports() # type: ignore
for component in self.component_map.values()
],
CodeBlock.create(theme="light")._get_imports(), # type: ignore,
Code.create()._get_imports(), # type: ignore,
]
def get_component(self, tag: str, **props) -> Component:
"""Get the component for a tag and props.
Args:
tag: The tag of the component.
**props: The props of the component.
Returns:
The component.
Raises:
ValueError: If the tag is invalid.
"""
# Check the tag is valid.
if tag not in self.component_map:
raise ValueError(f"No markdown component found for tag: {tag}.")
special_props = {_PROPS_IN_TAG}
children = [_CHILDREN]
# For certain tags, the props from the markdown renderer are not actually valid for the component.
if tag in NO_PROPS_TAGS:
special_props = set()
# If the children are set as a prop, don't pass them as children.
children_prop = props.pop("children", None)
if children_prop is not None:
special_props.add(
ImmutableVar.create_safe(f"children={{{str(children_prop)}}}")
)
children = []
# Get the component.
component = self.component_map[tag](*children, **props).set(
special_props=special_props
)
return component
def format_component(self, tag: str, **props) -> str:
"""Format a component for rendering in the component map.
Args:
tag: The tag of the component.
**props: Extra props to pass to the component function.
Returns:
The formatted component.
"""
return str(self.get_component(tag, **props)).replace("\n", "")
def format_component_map(self) -> dict[str, ImmutableVar]:
"""Format the component map for rendering.
Returns:
The formatted component map.
"""
components = {
tag: ImmutableVar.create_safe(
f"(({{node, {_CHILDREN._var_name}, {_PROPS._var_name}}}) => ({self.format_component(tag)}))"
)
for tag in self.component_map
}
# Separate out inline code and code blocks.
components["code"] = ImmutableVar.create_safe(
f"""(({{node, inline, className, {_CHILDREN._var_name}, {_PROPS._var_name}}}) => {{
const match = (className || '').match(/language-(?<lang>.*)/);
const language = match ? match[1] : '';
if (language) {{
(async () => {{
try {{
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{language}}`);
SyntaxHighlighter.registerLanguage(language, module.default);
}} catch (error) {{
console.error(`Error importing language module for ${{language}}:`, error);
}}
}})();
}}
return inline ? (
{self.format_component("code")}
) : (
{self.format_component("codeblock", language=ImmutableVar.create_safe("language"))}
);
}})""".replace("\n", " ")
)
return components
@staticmethod
def _component_map_hash(component_map) -> str:
inp = str(
{tag: component(_MOCK_ARG) for tag, component in component_map.items()}
).encode()
return md5(inp).hexdigest()
def _get_component_map_name(self) -> str:
return f"ComponentMap_{self.component_map_hash}"
def _get_custom_code(self) -> str | None:
hooks = set()
for _component in self.component_map.values():
comp = _component(_MOCK_ARG)
hooks.update(comp._get_all_hooks_internal())
hooks.update(comp._get_all_hooks())
formatted_hooks = "\n".join(hooks)
return f"""
function {self._get_component_map_name()} () {{
{formatted_hooks}
return (
{str(LiteralVar.create(self.format_component_map()))}
)
}}
"""
def _render(self) -> Tag:
tag = (
super()
._render()
.add_props(
remark_plugins=_REMARK_PLUGINS,
rehype_plugins=_REHYPE_PLUGINS,
components=ImmutableVar.create_safe(
f"{self._get_component_map_name()}()"
),
)
.remove_props("componentMap", "componentMapHash")
)
return tag