abstract the map var logic

This commit is contained in:
Elijah 2024-10-31 15:54:13 +00:00
parent 0b6988ba15
commit 827a171980
4 changed files with 180 additions and 74 deletions

View File

@ -8,14 +8,17 @@ from typing import ClassVar, Dict, Literal, Optional, Union
from reflex.components.component import Component, ComponentNamespace from reflex.components.component import Component, ComponentNamespace
from reflex.components.core.cond import color_mode_cond from reflex.components.core.cond import color_mode_cond
from reflex.components.lucide.icon import Icon from reflex.components.lucide.icon import Icon
from reflex.components.markdown.markdown import (
_LANGUAGE,
MarkdownComponentMap,
)
from reflex.components.radix.themes.components.button import Button from reflex.components.radix.themes.components.button import Button
from reflex.components.radix.themes.layout.box import Box from reflex.components.radix.themes.layout.box import Box
from reflex.components.markdown.markdown import MarkdownComponentMapMixin, _PROPS, _CHILDREN
from reflex.constants.colors import Color from reflex.constants.colors import Color
from reflex.event import set_clipboard from reflex.event import set_clipboard
from reflex.style import Style from reflex.style import Style
from reflex.utils import console, format from reflex.utils import console, format
from reflex.utils.imports import ImportDict, ImportVar from reflex.utils.imports import ImportVar
from reflex.vars.base import LiteralVar, Var, VarData from reflex.vars.base import LiteralVar, Var, VarData
LiteralCodeLanguage = Literal[ LiteralCodeLanguage = Literal[
@ -379,9 +382,7 @@ for theme_name in dir(Theme):
setattr(Theme, theme_name, getattr(Theme, theme_name)._replace(_var_type=Theme)) setattr(Theme, theme_name, getattr(Theme, theme_name)._replace(_var_type=Theme))
LANGUAGE_VAR = Var(_js_expr="__language") class CodeBlock(Component, MarkdownComponentMap):
class CodeBlock(Component, MarkdownComponentMapMixin):
"""A code block.""" """A code block."""
library = "react-syntax-highlighter@15.6.1" library = "react-syntax-highlighter@15.6.1"
@ -520,9 +521,8 @@ class CodeBlock(Component, MarkdownComponentMapMixin):
theme = self.theme theme = self.theme
out.add_props(style=theme).remove_props("theme", "code", "language").add_props( out.add_props(style=theme).remove_props("theme", "code", "language").add_props(
children=self.code, language=LANGUAGE_VAR children=self.code, language=_LANGUAGE
) )
return out return out
@ -530,6 +530,22 @@ class CodeBlock(Component, MarkdownComponentMapMixin):
def _exclude_props(self) -> list[str]: def _exclude_props(self) -> list[str]:
return ["can_copy", "copy_button"] return ["can_copy", "copy_button"]
@classmethod
def _get_language_registration_hook(cls) -> str:
"""Get the hook to register the language."""
return f"""
if ({str(_LANGUAGE)}) {{
(async () => {{
try {{
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{str(_LANGUAGE)}}}`);
SyntaxHighlighter.registerLanguage({str(_LANGUAGE)}, module.default);
}} catch (error) {{
console.error(`Error importing language module for ${{{str(_LANGUAGE)}}}:`, error);
}}
}})();
}}
"""
@classmethod @classmethod
def get_component_map_custom_code(cls) -> str: def get_component_map_custom_code(cls) -> str:
"""Get the custom code for the component. """Get the custom code for the component.
@ -537,21 +553,11 @@ class CodeBlock(Component, MarkdownComponentMapMixin):
Returns: Returns:
The custom code for the component. The custom code for the component.
""" """
return """ return f"""
const match = (className || '').match(/language-(?<lang>.*)/); const match = (className || '').match(/language-(?<lang>.*)/);
const language = match ? match[1] : ''; const {str(_LANGUAGE)} = match ? match[1] : '';
if (language) { {cls._get_language_registration_hook()}
(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);
}
})();
}
"""
def add_hooks(self) -> list[str | Var]: def add_hooks(self) -> list[str | Var]:
"""Add hooks for the component. """Add hooks for the component.
@ -560,19 +566,8 @@ const match = (className || '').match(/language-(?<lang>.*)/);
The hooks for the component. The hooks for the component.
""" """
return [ return [
f"const {str(LANGUAGE_VAR)} = {str(self.language)}", f"const {str(_LANGUAGE)} = {str(self.language)}",
f""" self._get_language_registration_hook(),
if ({str(LANGUAGE_VAR)}) {{
(async () => {{
try {{
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{str(LANGUAGE_VAR)}}}`);
SyntaxHighlighter.registerLanguage({str(LANGUAGE_VAR)}, module.default);
}} catch (error) {{
console.error(`Error importing language module for ${{{str(LANGUAGE_VAR)}}}:`, error);
}}
}})();
}}
"""
] ]

View File

@ -7,10 +7,10 @@ import dataclasses
from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload
from reflex.components.component import Component, ComponentNamespace from reflex.components.component import Component, ComponentNamespace
from reflex.components.markdown.markdown import MarkdownComponentMap
from reflex.constants.colors import Color from reflex.constants.colors import Color
from reflex.event import BASE_STATE, EventType from reflex.event import BASE_STATE, EventType
from reflex.style import Style from reflex.style import Style
from reflex.utils.imports import ImportDict
from reflex.vars.base import Var from reflex.vars.base import Var
LiteralCodeLanguage = Literal[ LiteralCodeLanguage = Literal[
@ -349,8 +349,7 @@ for theme_name in dir(Theme):
continue continue
setattr(Theme, theme_name, getattr(Theme, theme_name)._replace(_var_type=Theme)) setattr(Theme, theme_name, getattr(Theme, theme_name)._replace(_var_type=Theme))
class CodeBlock(Component): class CodeBlock(Component, MarkdownComponentMap):
def add_imports(self) -> ImportDict: ...
@overload @overload
@classmethod @classmethod
def create( # type: ignore def create( # type: ignore
@ -984,6 +983,9 @@ class CodeBlock(Component):
... ...
def add_style(self): ... def add_style(self): ...
@classmethod
def get_component_map_custom_code(cls) -> str: ...
def add_hooks(self) -> list[str | Var]: ...
class CodeblockNamespace(ComponentNamespace): class CodeblockNamespace(ComponentNamespace):
themes = Theme themes = Theme

View File

@ -28,6 +28,7 @@ _CHILDREN = Var(_js_expr="children", _var_type=str)
_PROPS = Var(_js_expr="...props") _PROPS = Var(_js_expr="...props")
_PROPS_IN_TAG = Var(_js_expr="{...props}") _PROPS_IN_TAG = Var(_js_expr="{...props}")
_MOCK_ARG = Var(_js_expr="", _var_type=str) _MOCK_ARG = Var(_js_expr="", _var_type=str)
_LANGUAGE = Var(_js_expr="_language", _var_type=str)
# Special remark plugins. # Special remark plugins.
_REMARK_MATH = Var(_js_expr="remarkMath") _REMARK_MATH = Var(_js_expr="remarkMath")
@ -74,10 +75,55 @@ def get_base_component_map() -> dict[str, Callable]:
} }
class MarkdownComponentMapMixin: class MarkdownComponentMap:
def get_component_map_custom_code(self) -> str: """Mixin class for handling custom component maps in Markdown components."""
@classmethod
def get_component_map_custom_code(cls) -> str:
"""Get the custom code for the component map.
Returns:
The custom code for the component map.
"""
return "" return ""
@classmethod
def create_map_fn_var(
cls, fn_body: str | None = None, fn_args: list | None = None
) -> Var:
"""Create a function Var for the component map.
Args:
fn_body: The formatted component as a string.
fn_args: The function arguments.
Returns:
The function Var for the component map.
"""
fn_args = fn_args or cls.get_fn_args()
fn_body = fn_body or cls.get_fn_body()
fn_args_str = ", ".join(fn_args)
return Var(_js_expr=f"(({{{fn_args_str}}}) => {fn_body})")
@classmethod
def get_fn_args(cls) -> list[str]:
"""Get the function arguments for the component map.
Returns:
The function arguments as a list of strings.
"""
return ["node", _CHILDREN._js_expr, _PROPS._js_expr]
@classmethod
def get_fn_body(cls) -> str:
"""Get the function body for the component map.
Returns:
The function body as a string.
"""
return "()"
class Markdown(Component): class Markdown(Component):
"""A markdown component.""" """A markdown component."""
@ -158,9 +204,6 @@ class Markdown(Component):
Returns: Returns:
The imports for the markdown component. The imports for the markdown component.
""" """
from reflex.components.datadisplay.code import CodeBlock, Theme
from reflex.components.radix.themes.typography.code import Code
return [ return [
{ {
"": "katex/dist/katex.min.css", "": "katex/dist/katex.min.css",
@ -184,10 +227,67 @@ class Markdown(Component):
component(_MOCK_ARG)._get_all_imports() # type: ignore component(_MOCK_ARG)._get_all_imports() # type: ignore
for component in self.component_map.values() for component in self.component_map.values()
], ],
# CodeBlock.create(theme=Theme.light)._get_imports(),
# Code.create()._get_imports(),
] ]
def _get_tag_map_fn_var(self, tag: str) -> Var:
return self._get_map_fn_var_from_children(self.get_component(tag), tag)
def format_component_map(self) -> dict[str, Var]:
"""Format the component map for rendering.
Returns:
The formatted component map.
"""
components = {
tag: self._get_tag_map_fn_var(tag)
for tag in self.component_map
if tag not in ("code", "codeblock")
}
# Separate out inline code and code blocks.
components["code"] = self._get_inline_code_fn_var()
return components
def _get_inline_code_fn_var(self) -> Var:
"""Get the function variable for inline code.
This function creates a Var that represents a function to handle
both inline code and code blocks in markdown.
Returns:
The Var for inline code.
"""
# Get any custom code from the codeblock and code components.
custom_code_list = self._get_map_fn_custom_code_from_children(
self.get_component("codeblock")
)
custom_code_list.extend(
self._get_map_fn_custom_code_from_children(self.get_component("code"))
)
codeblock_custom_code = "\n".join(custom_code_list)
# Format the code to handle inline and block code.
formatted_code = f"""{{{codeblock_custom_code};
return inline ? (
{self.format_component("code")}
) : (
{self.format_component("codeblock", language=_LANGUAGE)}
);
}}""".replace("\n", " ")
return MarkdownComponentMap.create_map_fn_var(
fn_args=[
"node",
"inline",
"className",
_CHILDREN._js_expr,
_PROPS._js_expr,
],
fn_body=formatted_code,
)
def get_component(self, tag: str, **props) -> Component: def get_component(self, tag: str, **props) -> Component:
"""Get the component for a tag and props. """Get the component for a tag and props.
@ -244,36 +344,23 @@ class Markdown(Component):
""" """
return str(self.get_component(tag, **props)).replace("\n", "") return str(self.get_component(tag, **props)).replace("\n", "")
def format_component_map(self) -> dict[str, Var]: def _get_map_fn_var_from_children(self, component: Component, tag: str) -> Var:
"""Format the component map for rendering. """Create a function Var for the component map for the specified tag.
Args:
component: The component to check for custom code.
tag: The tag of the component.
Returns: Returns:
The formatted component map. The function Var for the component map.
""" """
components = { if isinstance(component, MarkdownComponentMap):
tag: Var( return component.create_map_fn_var(f"({self.format_component(tag)})")
_js_expr=f"(({{node, {_CHILDREN._js_expr}, {_PROPS._js_expr}}}) => ({self.format_component(tag)}))"
)
for tag in self.component_map
}
codeblock_component = self.get_component("codeblock")
custom_code_list = self._get_custom_code_from_children(codeblock_component)
codeblock_custom_code = "\n".join(custom_code_list)
# Separate out inline code and code blocks.
components["code"] = Var(
_js_expr=f"""(({{node, inline, className, {_CHILDREN._js_expr}, {_PROPS._js_expr}}}) => {{
{codeblock_custom_code};
return inline ? (
{self.format_component("code")}
) : (
{self.format_component("codeblock", language=Var(_js_expr="language", _var_type=str))}
);
}})""".replace("\n", " ")
)
return components # fallback to the default fn Var creation if the component is not a MarkdownComponentMap.
return MarkdownComponentMap.create_map_fn_var(f"({self.format_component(tag)})")
def _get_custom_code_from_children(self, component) -> list[str]: def _get_map_fn_custom_code_from_children(self, component) -> list[str]:
"""Recursively get markdown custom code from children components. """Recursively get markdown custom code from children components.
Args: Args:
@ -283,16 +370,25 @@ class Markdown(Component):
A list of markdown custom code strings. A list of markdown custom code strings.
""" """
custom_code_list = [] custom_code_list = []
if hasattr(component, "get_component_map_custom_code"): if isinstance(component, MarkdownComponentMap):
custom_code_list.append(component.get_component_map_custom_code()) custom_code_list.append(component.get_component_map_custom_code())
# If the component is a custom component(rx.memo), obtain the underlining
# component and get the custom code from the children.
if isinstance(component, CustomComponent): if isinstance(component, CustomComponent):
custom_code_list.extend(self._get_custom_code_from_children(component.component_fn(*component.get_prop_vars()))) custom_code_list.extend(
self._get_map_fn_custom_code_from_children(
component.component_fn(*component.get_prop_vars())
)
)
else: else:
for child in component.children: for child in component.children:
custom_code_list.extend(self._get_custom_code_from_children(child)) custom_code_list.extend(
self._get_map_fn_custom_code_from_children(child)
)
return custom_code_list return custom_code_list
@staticmethod @staticmethod
def _component_map_hash(component_map) -> str: def _component_map_hash(component_map) -> str:
inp = str( inp = str(

View File

@ -16,6 +16,7 @@ _CHILDREN = Var(_js_expr="children", _var_type=str)
_PROPS = Var(_js_expr="...props") _PROPS = Var(_js_expr="...props")
_PROPS_IN_TAG = Var(_js_expr="{...props}") _PROPS_IN_TAG = Var(_js_expr="{...props}")
_MOCK_ARG = Var(_js_expr="", _var_type=str) _MOCK_ARG = Var(_js_expr="", _var_type=str)
_LANGUAGE = Var(_js_expr="_language", _var_type=str)
_REMARK_MATH = Var(_js_expr="remarkMath") _REMARK_MATH = Var(_js_expr="remarkMath")
_REMARK_GFM = Var(_js_expr="remarkGfm") _REMARK_GFM = Var(_js_expr="remarkGfm")
_REMARK_UNWRAP_IMAGES = Var(_js_expr="remarkUnwrapImages") _REMARK_UNWRAP_IMAGES = Var(_js_expr="remarkUnwrapImages")
@ -28,6 +29,18 @@ NO_PROPS_TAGS = ("ul", "ol", "li")
@lru_cache @lru_cache
def get_base_component_map() -> dict[str, Callable]: ... def get_base_component_map() -> dict[str, Callable]: ...
class MarkdownComponentMap:
@classmethod
def get_component_map_custom_code(cls) -> str: ...
@classmethod
def create_map_fn_var(
cls, fn_body: str | None = None, fn_args: list | None = None
) -> Var: ...
@classmethod
def get_fn_args(cls) -> list[str]: ...
@classmethod
def get_fn_body(cls) -> str: ...
class Markdown(Component): class Markdown(Component):
@overload @overload
@classmethod @classmethod
@ -82,6 +95,6 @@ class Markdown(Component):
... ...
def add_imports(self) -> ImportDict | list[ImportDict]: ... def add_imports(self) -> ImportDict | list[ImportDict]: ...
def format_component_map(self) -> dict[str, Var]: ...
def get_component(self, tag: str, **props) -> Component: ... def get_component(self, tag: str, **props) -> Component: ...
def format_component(self, tag: str, **props) -> str: ... def format_component(self, tag: str, **props) -> str: ...
def format_component_map(self) -> dict[str, Var]: ...