test_markdown.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import pytest
  2. from reflex.components.component import Component, memo
  3. from reflex.components.datadisplay.code import CodeBlock
  4. from reflex.components.datadisplay.shiki_code_block import ShikiHighLevelCodeBlock
  5. from reflex.components.markdown.markdown import Markdown, MarkdownComponentMap
  6. from reflex.components.radix.themes.layout.box import Box
  7. from reflex.components.radix.themes.typography.heading import Heading
  8. from reflex.vars.base import Var
  9. class CustomMarkdownComponent(Component, MarkdownComponentMap):
  10. """A custom markdown component."""
  11. tag = "CustomMarkdownComponent"
  12. library = "custom"
  13. @classmethod
  14. def get_fn_args(cls) -> tuple[str, ...]:
  15. """Return the function arguments.
  16. Returns:
  17. The function arguments.
  18. """
  19. return ("custom_node", "custom_children", "custom_props")
  20. @classmethod
  21. def get_fn_body(cls) -> Var:
  22. """Return the function body.
  23. Returns:
  24. The function body.
  25. """
  26. return Var(_js_expr="{return custom_node + custom_children + custom_props}")
  27. def syntax_highlighter_memoized_component(codeblock: type[Component]):
  28. @memo
  29. def code_block(code: str, language: str):
  30. return Box.create(
  31. codeblock.create(
  32. code,
  33. language=language,
  34. class_name="code-block",
  35. can_copy=True,
  36. ),
  37. class_name="relative mb-4",
  38. )
  39. def code_block_markdown(*children, **props):
  40. return code_block(
  41. code=children[0], language=props.pop("language", "plain"), **props
  42. )
  43. return code_block_markdown
  44. @pytest.mark.parametrize(
  45. "fn_body, fn_args, explicit_return, expected",
  46. [
  47. (
  48. None,
  49. None,
  50. False,
  51. Var(_js_expr="(({node, children, ...props}) => undefined)"),
  52. ),
  53. ("return node", ("node",), True, Var(_js_expr="(({node}) => {return node})")),
  54. (
  55. "return node + children",
  56. ("node", "children"),
  57. True,
  58. Var(_js_expr="(({node, children}) => {return node + children})"),
  59. ),
  60. (
  61. "return node + props",
  62. ("node", "...props"),
  63. True,
  64. Var(_js_expr="(({node, ...props}) => {return node + props})"),
  65. ),
  66. (
  67. "return node + children + props",
  68. ("node", "children", "...props"),
  69. True,
  70. Var(
  71. _js_expr="(({node, children, ...props}) => {return node + children + props})"
  72. ),
  73. ),
  74. ],
  75. )
  76. def test_create_map_fn_var(fn_body, fn_args, explicit_return, expected):
  77. result = MarkdownComponentMap.create_map_fn_var(
  78. fn_body=Var(_js_expr=fn_body, _var_type=str) if fn_body else None,
  79. fn_args=fn_args,
  80. explicit_return=explicit_return,
  81. )
  82. assert result._js_expr == expected._js_expr
  83. @pytest.mark.parametrize(
  84. ("cls", "fn_body", "fn_args", "explicit_return", "expected"),
  85. [
  86. (
  87. MarkdownComponentMap,
  88. None,
  89. None,
  90. False,
  91. Var(_js_expr="(({node, children, ...props}) => undefined)"),
  92. ),
  93. (
  94. MarkdownComponentMap,
  95. "return node",
  96. ("node",),
  97. True,
  98. Var(_js_expr="(({node}) => {return node})"),
  99. ),
  100. (
  101. CustomMarkdownComponent,
  102. None,
  103. None,
  104. True,
  105. Var(
  106. _js_expr="(({custom_node, custom_children, custom_props}) => {return custom_node + custom_children + custom_props})"
  107. ),
  108. ),
  109. (
  110. CustomMarkdownComponent,
  111. "return custom_node",
  112. ("custom_node",),
  113. True,
  114. Var(_js_expr="(({custom_node}) => {return custom_node})"),
  115. ),
  116. ],
  117. )
  118. def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expected):
  119. result = cls.create_map_fn_var(
  120. fn_body=Var(_js_expr=fn_body, _var_type=int) if fn_body else None,
  121. fn_args=fn_args,
  122. explicit_return=explicit_return,
  123. )
  124. assert result._js_expr == expected._js_expr
  125. @pytest.mark.parametrize(
  126. "key,component_map, expected",
  127. [
  128. (
  129. "code",
  130. {},
  131. 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 ? ( jsx(RadixThemesCode,{...props},children,) ) : ( jsx(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},) ); })""",
  132. ),
  133. (
  134. "code",
  135. {
  136. "codeblock": lambda value, **props: ShikiHighLevelCodeBlock.create(
  137. value, **props
  138. )
  139. },
  140. r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?<lang>.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children,) ) : ( jsx(RadixThemesBox,{css:({ ["pre"] : ({ ["margin"] : "0", ["padding"] : "24px", ["background"] : "transparent", ["overflowX"] : "auto", ["borderRadius"] : "6px" }) }),...props},jsx(ShikiCode,{code:((Array.isArray(children)) ? children.join("\n") : children),decorations:[],language:_language,theme:((resolvedColorMode === "light") ? "one-light" : "one-dark-pro"),transformers:[]},),) ); })""",
  141. ),
  142. (
  143. "h1",
  144. {
  145. "h1": lambda value: CustomMarkdownComponent.create(
  146. Heading.create(value, as_="h1", size="6", margin_y="0.5em")
  147. )
  148. },
  149. """(({custom_node, custom_children, custom_props}) => (jsx(CustomMarkdownComponent,{...props},jsx(RadixThemesHeading,{as:"h1",css:({ ["marginTop"] : "0.5em", ["marginBottom"] : "0.5em" }),size:"6"},children,),)))""",
  150. ),
  151. (
  152. "code",
  153. {"codeblock": syntax_highlighter_memoized_component(CodeBlock)},
  154. 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 ? ( jsx(RadixThemesCode,{...props},children,) ) : ( jsx(CodeBlock,{code:((Array.isArray(children)) ? children.join("\n") : children),language:_language,...props},) ); })""",
  155. ),
  156. (
  157. "code",
  158. {
  159. "codeblock": syntax_highlighter_memoized_component(
  160. ShikiHighLevelCodeBlock
  161. )
  162. },
  163. r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?<lang>.*)/); let _language = match ? match[1] : ''; ; return inline ? ( jsx(RadixThemesCode,{...props},children,) ) : ( jsx(CodeBlock,{code:((Array.isArray(children)) ? children.join("\n") : children),language:_language,...props},) ); })""",
  164. ),
  165. ],
  166. )
  167. def test_markdown_format_component(key, component_map, expected):
  168. markdown = Markdown.create("# header", component_map=component_map)
  169. result = markdown.format_component_map()
  170. print(str(result[key]))
  171. assert str(result[key]) == expected