improve behavior on missing language with markdown code blocks (#4750)

* improve behavior on missing language with markdown code blocks

* special case on literal var

* fix tests

* missing f

* remove extra throw
This commit is contained in:
Khaleel Al-Adhami 2025-02-06 10:09:26 -08:00 committed by GitHub
parent 1651289485
commit 9d23271c14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 24 deletions

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
import typing
from typing import ClassVar, Dict, Literal, Optional, Union from typing import ClassVar, Dict, Literal, Optional, Union
from reflex.components.component import Component, ComponentNamespace from reflex.components.component import Component, ComponentNamespace
@ -503,7 +504,7 @@ class CodeBlock(Component, MarkdownComponentMap):
return ["can_copy", "copy_button"] return ["can_copy", "copy_button"]
@classmethod @classmethod
def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> str: def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> Var:
"""Get the hook to register the language. """Get the hook to register the language.
Args: Args:
@ -514,21 +515,46 @@ class CodeBlock(Component, MarkdownComponentMap):
Returns: Returns:
The hook to register the language. The hook to register the language.
""" """
return f""" language_in_there = Var.create(typing.get_args(LiteralCodeLanguage)).contains(
if ({language_var!s}) {{ language_var
(async () => {{ )
try {{ async_load = f"""
(async () => {{
try {{
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`); const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`);
SyntaxHighlighter.registerLanguage({language_var!s}, module.default); SyntaxHighlighter.registerLanguage({language_var!s}, module.default);
}} catch (error) {{ }} catch (error) {{
console.error(`Error importing language module for ${{{language_var!s}}}:`, error); console.error(`Language ${{{language_var!s}}} is not supported for code blocks inside of markdown: `, error);
}} }}
}})(); }})();
"""
return Var(
f"""
if ({language_var!s}) {{
if (!{language_in_there!s}) {{
console.warn(`Language \\`${{{language_var!s}}}\\` is not supported for code blocks inside of markdown.`);
{language_var!s} = '';
}} else {{
{async_load!s}
}}
}} }}
""" """
if not isinstance(language_var, LiteralVar)
else f"""
if ({language_var!s}) {{
{async_load!s}
}}""",
_var_data=VarData(
imports={
cls.__fields__["library"].default: [
ImportVar(tag="PrismAsyncLight", alias="SyntaxHighlighter")
]
},
),
)
@classmethod @classmethod
def get_component_map_custom_code(cls) -> str: def get_component_map_custom_code(cls) -> Var:
"""Get the custom code for the component. """Get the custom code for the component.
Returns: Returns:

View File

@ -984,7 +984,7 @@ class CodeBlock(Component, MarkdownComponentMap):
def add_style(self): ... def add_style(self): ...
@classmethod @classmethod
def get_component_map_custom_code(cls) -> str: ... def get_component_map_custom_code(cls) -> Var: ...
def add_hooks(self) -> list[str | Var]: ... def add_hooks(self) -> list[str | Var]: ...
class CodeblockNamespace(ComponentNamespace): class CodeblockNamespace(ComponentNamespace):

View File

@ -12,7 +12,7 @@ from reflex.components.component import BaseComponent, Component, CustomComponen
from reflex.components.tags.tag import Tag from reflex.components.tags.tag import Tag
from reflex.utils import types from reflex.utils import types
from reflex.utils.imports import ImportDict, ImportVar from reflex.utils.imports import ImportDict, ImportVar
from reflex.vars.base import LiteralVar, Var from reflex.vars.base import LiteralVar, Var, VarData
from reflex.vars.function import ARRAY_ISARRAY, ArgsFunctionOperation, DestructuredArg from reflex.vars.function import ARRAY_ISARRAY, ArgsFunctionOperation, DestructuredArg
from reflex.vars.number import ternary_operation from reflex.vars.number import ternary_operation
@ -83,13 +83,13 @@ class MarkdownComponentMap:
_explicit_return: bool = dataclasses.field(default=False) _explicit_return: bool = dataclasses.field(default=False)
@classmethod @classmethod
def get_component_map_custom_code(cls) -> str: def get_component_map_custom_code(cls) -> Var:
"""Get the custom code for the component map. """Get the custom code for the component map.
Returns: Returns:
The custom code for the component map. The custom code for the component map.
""" """
return "" return Var("")
@classmethod @classmethod
def create_map_fn_var( def create_map_fn_var(
@ -97,6 +97,7 @@ class MarkdownComponentMap:
fn_body: Var | None = None, fn_body: Var | None = None,
fn_args: Sequence[str] | None = None, fn_args: Sequence[str] | None = None,
explicit_return: bool | None = None, explicit_return: bool | None = None,
var_data: VarData | None = None,
) -> Var: ) -> Var:
"""Create a function Var for the component map. """Create a function Var for the component map.
@ -104,6 +105,7 @@ class MarkdownComponentMap:
fn_body: The formatted component as a string. fn_body: The formatted component as a string.
fn_args: The function arguments. fn_args: The function arguments.
explicit_return: Whether to use explicit return syntax. explicit_return: Whether to use explicit return syntax.
var_data: The var data for the function.
Returns: Returns:
The function Var for the component map. The function Var for the component map.
@ -116,6 +118,7 @@ class MarkdownComponentMap:
args_names=(DestructuredArg(fields=tuple(fn_args)),), args_names=(DestructuredArg(fields=tuple(fn_args)),),
return_expr=fn_body, return_expr=fn_body,
explicit_return=explicit_return, explicit_return=explicit_return,
_var_data=var_data,
) )
@classmethod @classmethod
@ -239,6 +242,15 @@ class Markdown(Component):
component(_MOCK_ARG)._get_all_imports() component(_MOCK_ARG)._get_all_imports()
for component in self.component_map.values() for component in self.component_map.values()
], ],
*(
[inline_code_var_data.old_school_imports()]
if (
inline_code_var_data
:= self._get_inline_code_fn_var()._get_all_var_data()
)
is not None
else []
),
] ]
def _get_tag_map_fn_var(self, tag: str) -> Var: def _get_tag_map_fn_var(self, tag: str) -> Var:
@ -278,12 +290,20 @@ class Markdown(Component):
self._get_map_fn_custom_code_from_children(self.get_component("code")) self._get_map_fn_custom_code_from_children(self.get_component("code"))
) )
codeblock_custom_code = "\n".join(custom_code_list) var_data = VarData.merge(
*[
code._get_all_var_data()
for code in custom_code_list
if isinstance(code, Var)
]
)
codeblock_custom_code = "\n".join(map(str, custom_code_list))
# Format the code to handle inline and block code. # Format the code to handle inline and block code.
formatted_code = f""" formatted_code = f"""
const match = (className || '').match(/language-(?<lang>.*)/); const match = (className || '').match(/language-(?<lang>.*)/);
const {_LANGUAGE!s} = match ? match[1] : ''; let {_LANGUAGE!s} = match ? match[1] : '';
{codeblock_custom_code}; {codeblock_custom_code};
return inline ? ( return inline ? (
{self.format_component("code")} {self.format_component("code")}
@ -302,6 +322,7 @@ const {_LANGUAGE!s} = match ? match[1] : '';
), ),
fn_body=Var(_js_expr=formatted_code), fn_body=Var(_js_expr=formatted_code),
explicit_return=True, explicit_return=True,
var_data=var_data,
) )
def get_component(self, tag: str, **props) -> Component: def get_component(self, tag: str, **props) -> Component:
@ -381,7 +402,7 @@ const {_LANGUAGE!s} = match ? match[1] : '';
def _get_map_fn_custom_code_from_children( def _get_map_fn_custom_code_from_children(
self, component: BaseComponent self, component: BaseComponent
) -> list[str]: ) -> list[str | Var]:
"""Recursively get markdown custom code from children components. """Recursively get markdown custom code from children components.
Args: Args:
@ -390,7 +411,7 @@ const {_LANGUAGE!s} = match ? match[1] : '';
Returns: Returns:
A list of markdown custom code strings. A list of markdown custom code strings.
""" """
custom_code_list = [] custom_code_list: list[str | Var] = []
if isinstance(component, MarkdownComponentMap): if isinstance(component, MarkdownComponentMap):
custom_code_list.append(component.get_component_map_custom_code()) custom_code_list.append(component.get_component_map_custom_code())

View File

@ -11,7 +11,7 @@ from reflex.components.component import Component
from reflex.event import EventType from reflex.event import EventType
from reflex.style import Style from reflex.style import Style
from reflex.utils.imports import ImportDict from reflex.utils.imports import ImportDict
from reflex.vars.base import LiteralVar, Var from reflex.vars.base import LiteralVar, Var, VarData
_CHILDREN = Var(_js_expr="children", _var_type=str) _CHILDREN = Var(_js_expr="children", _var_type=str)
_PROPS = Var(_js_expr="...props") _PROPS = Var(_js_expr="...props")
@ -32,13 +32,14 @@ def get_base_component_map() -> dict[str, Callable]: ...
@dataclasses.dataclass() @dataclasses.dataclass()
class MarkdownComponentMap: class MarkdownComponentMap:
@classmethod @classmethod
def get_component_map_custom_code(cls) -> str: ... def get_component_map_custom_code(cls) -> Var: ...
@classmethod @classmethod
def create_map_fn_var( def create_map_fn_var(
cls, cls,
fn_body: Var | None = None, fn_body: Var | None = None,
fn_args: Sequence[str] | None = None, fn_args: Sequence[str] | None = None,
explicit_return: bool | None = None, explicit_return: bool | None = None,
var_data: VarData | None = None,
) -> Var: ... ) -> Var: ...
@classmethod @classmethod
def get_fn_args(cls) -> Sequence[str]: ... def get_fn_args(cls) -> Sequence[str]: ...

View File

@ -148,7 +148,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe
( (
"code", "code",
{}, {},
"""(({node, inline, className, children, ...props}) => { 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 ? ( <RadixThemesCode {...props}>{children}</RadixThemesCode> ) : ( <SyntaxHighlighter children={((Array.isArray(children)) ? children.join("\\n") : children)} css={({ ["marginTop"] : "1em", ["marginBottom"] : "1em" })} customStyle={({ ["marginTop"] : "1em", ["marginBottom"] : "1em" })} language={_language} style={((resolvedColorMode === "light") ? oneLight : oneDark)} wrapLongLines={true} {...props}/> ); })""", r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?<lang>.*)/); let _language = match ? match[1] : ''; if (_language) { if (!["abap", "abnf", "actionscript", "ada", "agda", "al", "antlr4", "apacheconf", "apex", "apl", "applescript", "aql", "arduino", "arff", "asciidoc", "asm6502", "asmatmel", "aspnet", "autohotkey", "autoit", "avisynth", "avro-idl", "bash", "basic", "batch", "bbcode", "bicep", "birb", "bison", "bnf", "brainfuck", "brightscript", "bro", "bsl", "c", "cfscript", "chaiscript", "cil", "clike", "clojure", "cmake", "cobol", "coffeescript", "concurnas", "coq", "core", "cpp", "crystal", "csharp", "cshtml", "csp", "css", "css-extras", "csv", "cypher", "d", "dart", "dataweave", "dax", "dhall", "diff", "django", "dns-zone-file", "docker", "dot", "ebnf", "editorconfig", "eiffel", "ejs", "elixir", "elm", "erb", "erlang", "etlua", "excel-formula", "factor", "false", "firestore-security-rules", "flow", "fortran", "fsharp", "ftl", "gap", "gcode", "gdscript", "gedcom", "gherkin", "git", "glsl", "gml", "gn", "go", "go-module", "graphql", "groovy", "haml", "handlebars", "haskell", "haxe", "hcl", "hlsl", "hoon", "hpkp", "hsts", "http", "ichigojam", "icon", "icu-message-format", "idris", "iecst", "ignore", "index", "inform7", "ini", "io", "j", "java", "javadoc", "javadoclike", "javascript", "javastacktrace", "jexl", "jolie", "jq", "js-extras", "js-templates", "jsdoc", "json", "json5", "jsonp", "jsstacktrace", "jsx", "julia", "keepalived", "keyman", "kotlin", "kumir", "kusto", "latex", "latte", "less", "lilypond", "liquid", "lisp", "livescript", "llvm", "log", "lolcode", "lua", "magma", "makefile", "markdown", "markup", "markup-templating", "matlab", "maxscript", "mel", "mermaid", "mizar", "mongodb", "monkey", "moonscript", "n1ql", "n4js", "nand2tetris-hdl", "naniscript", "nasm", "neon", "nevod", "nginx", "nim", "nix", "nsis", "objectivec", "ocaml", "opencl", "openqasm", "oz", "parigp", "parser", "pascal", "pascaligo", "pcaxis", "peoplecode", "perl", "php", "php-extras", "phpdoc", "plsql", "powerquery", "powershell", "processing", "prolog", "promql", "properties", "protobuf", "psl", "pug", "puppet", "pure", "purebasic", "purescript", "python", "q", "qml", "qore", "qsharp", "r", "racket", "reason", "regex", "rego", "renpy", "rest", "rip", "roboconf", "robotframework", "ruby", "rust", "sas", "sass", "scala", "scheme", "scss", "shell-session", "smali", "smalltalk", "smarty", "sml", "solidity", "solution-file", "soy", "sparql", "splunk-spl", "sqf", "sql", "squirrel", "stan", "stylus", "swift", "systemd", "t4-cs", "t4-templating", "t4-vb", "tap", "tcl", "textile", "toml", "tremor", "tsx", "tt2", "turtle", "twig", "typescript", "typoscript", "unrealscript", "uorazor", "uri", "v", "vala", "vbnet", "velocity", "verilog", "vhdl", "vim", "visual-basic", "warpscript", "wasm", "web-idl", "wiki", "wolfram", "wren", "xeora", "xml-doc", "xojo", "xquery", "yaml", "yang", "zig"].includes(_language)) { console.warn(`Language \`${_language}\` is not supported for code blocks inside of markdown.`); _language = ''; } else { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Language ${_language} is not supported for code blocks inside of markdown: `, error); } })(); } } ; return inline ? ( <RadixThemesCode {...props}>{children}</RadixThemesCode> ) : ( <SyntaxHighlighter children={((Array.isArray(children)) ? children.join("\n") : children)} css={({ ["marginTop"] : "1em", ["marginBottom"] : "1em" })} customStyle={({ ["marginTop"] : "1em", ["marginBottom"] : "1em" })} language={_language} style={((resolvedColorMode === "light") ? oneLight : oneDark)} wrapLongLines={true} {...props}/> ); })""",
), ),
( (
"code", "code",
@ -157,7 +157,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe
value, **props value, **props
) )
}, },
"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?<lang>.*)/); const _language = match ? match[1] : ''; ; return inline ? ( <RadixThemesCode {...props}>{children}</RadixThemesCode> ) : ( <RadixThemesBox css={({ ["pre"] : ({ ["margin"] : "0", ["padding"] : "24px", ["background"] : "transparent", ["overflow-x"] : "auto", ["border-radius"] : "6px" }) })} {...props}><ShikiCode code={((Array.isArray(children)) ? children.join("\\n") : children)} decorations={[]} language={_language} theme={((resolvedColorMode === "light") ? "one-light" : "one-dark-pro")} transformers={[]}/></RadixThemesBox> ); })""", r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?<lang>.*)/); let _language = match ? match[1] : ''; ; return inline ? ( <RadixThemesCode {...props}>{children}</RadixThemesCode> ) : ( <RadixThemesBox css={({ ["pre"] : ({ ["margin"] : "0", ["padding"] : "24px", ["background"] : "transparent", ["overflow-x"] : "auto", ["border-radius"] : "6px" }) })} {...props}><ShikiCode code={((Array.isArray(children)) ? children.join("\n") : children)} decorations={[]} language={_language} theme={((resolvedColorMode === "light") ? "one-light" : "one-dark-pro")} transformers={[]}/></RadixThemesBox> ); })""",
), ),
( (
"h1", "h1",
@ -171,7 +171,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe
( (
"code", "code",
{"codeblock": syntax_highlighter_memoized_component(CodeBlock)}, {"codeblock": syntax_highlighter_memoized_component(CodeBlock)},
"""(({node, inline, className, children, ...props}) => { 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 ? ( <RadixThemesCode {...props}>{children}</RadixThemesCode> ) : ( <CodeBlock code={((Array.isArray(children)) ? children.join("\\n") : children)} language={_language} {...props}/> ); })""", r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?<lang>.*)/); let _language = match ? match[1] : ''; if (_language) { if (!["abap", "abnf", "actionscript", "ada", "agda", "al", "antlr4", "apacheconf", "apex", "apl", "applescript", "aql", "arduino", "arff", "asciidoc", "asm6502", "asmatmel", "aspnet", "autohotkey", "autoit", "avisynth", "avro-idl", "bash", "basic", "batch", "bbcode", "bicep", "birb", "bison", "bnf", "brainfuck", "brightscript", "bro", "bsl", "c", "cfscript", "chaiscript", "cil", "clike", "clojure", "cmake", "cobol", "coffeescript", "concurnas", "coq", "core", "cpp", "crystal", "csharp", "cshtml", "csp", "css", "css-extras", "csv", "cypher", "d", "dart", "dataweave", "dax", "dhall", "diff", "django", "dns-zone-file", "docker", "dot", "ebnf", "editorconfig", "eiffel", "ejs", "elixir", "elm", "erb", "erlang", "etlua", "excel-formula", "factor", "false", "firestore-security-rules", "flow", "fortran", "fsharp", "ftl", "gap", "gcode", "gdscript", "gedcom", "gherkin", "git", "glsl", "gml", "gn", "go", "go-module", "graphql", "groovy", "haml", "handlebars", "haskell", "haxe", "hcl", "hlsl", "hoon", "hpkp", "hsts", "http", "ichigojam", "icon", "icu-message-format", "idris", "iecst", "ignore", "index", "inform7", "ini", "io", "j", "java", "javadoc", "javadoclike", "javascript", "javastacktrace", "jexl", "jolie", "jq", "js-extras", "js-templates", "jsdoc", "json", "json5", "jsonp", "jsstacktrace", "jsx", "julia", "keepalived", "keyman", "kotlin", "kumir", "kusto", "latex", "latte", "less", "lilypond", "liquid", "lisp", "livescript", "llvm", "log", "lolcode", "lua", "magma", "makefile", "markdown", "markup", "markup-templating", "matlab", "maxscript", "mel", "mermaid", "mizar", "mongodb", "monkey", "moonscript", "n1ql", "n4js", "nand2tetris-hdl", "naniscript", "nasm", "neon", "nevod", "nginx", "nim", "nix", "nsis", "objectivec", "ocaml", "opencl", "openqasm", "oz", "parigp", "parser", "pascal", "pascaligo", "pcaxis", "peoplecode", "perl", "php", "php-extras", "phpdoc", "plsql", "powerquery", "powershell", "processing", "prolog", "promql", "properties", "protobuf", "psl", "pug", "puppet", "pure", "purebasic", "purescript", "python", "q", "qml", "qore", "qsharp", "r", "racket", "reason", "regex", "rego", "renpy", "rest", "rip", "roboconf", "robotframework", "ruby", "rust", "sas", "sass", "scala", "scheme", "scss", "shell-session", "smali", "smalltalk", "smarty", "sml", "solidity", "solution-file", "soy", "sparql", "splunk-spl", "sqf", "sql", "squirrel", "stan", "stylus", "swift", "systemd", "t4-cs", "t4-templating", "t4-vb", "tap", "tcl", "textile", "toml", "tremor", "tsx", "tt2", "turtle", "twig", "typescript", "typoscript", "unrealscript", "uorazor", "uri", "v", "vala", "vbnet", "velocity", "verilog", "vhdl", "vim", "visual-basic", "warpscript", "wasm", "web-idl", "wiki", "wolfram", "wren", "xeora", "xml-doc", "xojo", "xquery", "yaml", "yang", "zig"].includes(_language)) { console.warn(`Language \`${_language}\` is not supported for code blocks inside of markdown.`); _language = ''; } else { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Language ${_language} is not supported for code blocks inside of markdown: `, error); } })(); } } ; return inline ? ( <RadixThemesCode {...props}>{children}</RadixThemesCode> ) : ( <CodeBlock code={((Array.isArray(children)) ? children.join("\n") : children)} language={_language} {...props}/> ); })""",
), ),
( (
"code", "code",
@ -180,11 +180,12 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe
ShikiHighLevelCodeBlock ShikiHighLevelCodeBlock
) )
}, },
"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?<lang>.*)/); const _language = match ? match[1] : ''; ; return inline ? ( <RadixThemesCode {...props}>{children}</RadixThemesCode> ) : ( <CodeBlock code={((Array.isArray(children)) ? children.join("\\n") : children)} language={_language} {...props}/> ); })""", r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?<lang>.*)/); let _language = match ? match[1] : ''; ; return inline ? ( <RadixThemesCode {...props}>{children}</RadixThemesCode> ) : ( <CodeBlock code={((Array.isArray(children)) ? children.join("\n") : children)} language={_language} {...props}/> ); })""",
), ),
], ],
) )
def test_markdown_format_component(key, component_map, expected): def test_markdown_format_component(key, component_map, expected):
markdown = Markdown.create("# header", component_map=component_map) markdown = Markdown.create("# header", component_map=component_map)
result = markdown.format_component_map() result = markdown.format_component_map()
print(str(result[key]))
assert str(result[key]) == expected assert str(result[key]) == expected