code.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. """A code component."""
  2. from __future__ import annotations
  3. import dataclasses
  4. import typing
  5. from typing import ClassVar, Literal
  6. from reflex.components.component import Component, ComponentNamespace
  7. from reflex.components.core.cond import color_mode_cond
  8. from reflex.components.lucide.icon import Icon
  9. from reflex.components.markdown.markdown import _LANGUAGE, MarkdownComponentMap
  10. from reflex.components.radix.themes.components.button import Button
  11. from reflex.components.radix.themes.layout.box import Box
  12. from reflex.constants.colors import Color
  13. from reflex.event import set_clipboard
  14. from reflex.style import Style
  15. from reflex.utils import format
  16. from reflex.utils.imports import ImportVar
  17. from reflex.vars.base import LiteralVar, Var, VarData
  18. LiteralCodeLanguage = Literal[
  19. "abap",
  20. "abnf",
  21. "actionscript",
  22. "ada",
  23. "agda",
  24. "al",
  25. "antlr4",
  26. "apacheconf",
  27. "apex",
  28. "apl",
  29. "applescript",
  30. "aql",
  31. "arduino",
  32. "arff",
  33. "asciidoc",
  34. "asm6502",
  35. "asmatmel",
  36. "aspnet",
  37. "autohotkey",
  38. "autoit",
  39. "avisynth",
  40. "avro-idl",
  41. "bash",
  42. "basic",
  43. "batch",
  44. "bbcode",
  45. "bicep",
  46. "birb",
  47. "bison",
  48. "bnf",
  49. "brainfuck",
  50. "brightscript",
  51. "bro",
  52. "bsl",
  53. "c",
  54. "cfscript",
  55. "chaiscript",
  56. "cil",
  57. "clike",
  58. "clojure",
  59. "cmake",
  60. "cobol",
  61. "coffeescript",
  62. "concurnas",
  63. "coq",
  64. "core",
  65. "cpp",
  66. "crystal",
  67. "csharp",
  68. "cshtml",
  69. "csp",
  70. "css",
  71. "css-extras",
  72. "csv",
  73. "cypher",
  74. "d",
  75. "dart",
  76. "dataweave",
  77. "dax",
  78. "dhall",
  79. "diff",
  80. "django",
  81. "dns-zone-file",
  82. "docker",
  83. "dot",
  84. "ebnf",
  85. "editorconfig",
  86. "eiffel",
  87. "ejs",
  88. "elixir",
  89. "elm",
  90. "erb",
  91. "erlang",
  92. "etlua",
  93. "excel-formula",
  94. "factor",
  95. "false",
  96. "firestore-security-rules",
  97. "flow",
  98. "fortran",
  99. "fsharp",
  100. "ftl",
  101. "gap",
  102. "gcode",
  103. "gdscript",
  104. "gedcom",
  105. "gherkin",
  106. "git",
  107. "glsl",
  108. "gml",
  109. "gn",
  110. "go",
  111. "go-module",
  112. "graphql",
  113. "groovy",
  114. "haml",
  115. "handlebars",
  116. "haskell",
  117. "haxe",
  118. "hcl",
  119. "hlsl",
  120. "hoon",
  121. "hpkp",
  122. "hsts",
  123. "http",
  124. "ichigojam",
  125. "icon",
  126. "icu-message-format",
  127. "idris",
  128. "iecst",
  129. "ignore",
  130. "index",
  131. "inform7",
  132. "ini",
  133. "io",
  134. "j",
  135. "java",
  136. "javadoc",
  137. "javadoclike",
  138. "javascript",
  139. "javastacktrace",
  140. "jexl",
  141. "jolie",
  142. "jq",
  143. "js-extras",
  144. "js-templates",
  145. "jsdoc",
  146. "json",
  147. "json5",
  148. "jsonp",
  149. "jsstacktrace",
  150. "jsx",
  151. "julia",
  152. "keepalived",
  153. "keyman",
  154. "kotlin",
  155. "kumir",
  156. "kusto",
  157. "latex",
  158. "latte",
  159. "less",
  160. "lilypond",
  161. "liquid",
  162. "lisp",
  163. "livescript",
  164. "llvm",
  165. "log",
  166. "lolcode",
  167. "lua",
  168. "magma",
  169. "makefile",
  170. "markdown",
  171. "markup",
  172. "markup-templating",
  173. "matlab",
  174. "maxscript",
  175. "mel",
  176. "mermaid",
  177. "mizar",
  178. "mongodb",
  179. "monkey",
  180. "moonscript",
  181. "n1ql",
  182. "n4js",
  183. "nand2tetris-hdl",
  184. "naniscript",
  185. "nasm",
  186. "neon",
  187. "nevod",
  188. "nginx",
  189. "nim",
  190. "nix",
  191. "nsis",
  192. "objectivec",
  193. "ocaml",
  194. "opencl",
  195. "openqasm",
  196. "oz",
  197. "parigp",
  198. "parser",
  199. "pascal",
  200. "pascaligo",
  201. "pcaxis",
  202. "peoplecode",
  203. "perl",
  204. "php",
  205. "php-extras",
  206. "phpdoc",
  207. "plsql",
  208. "powerquery",
  209. "powershell",
  210. "processing",
  211. "prolog",
  212. "promql",
  213. "properties",
  214. "protobuf",
  215. "psl",
  216. "pug",
  217. "puppet",
  218. "pure",
  219. "purebasic",
  220. "purescript",
  221. "python",
  222. "q",
  223. "qml",
  224. "qore",
  225. "qsharp",
  226. "r",
  227. "racket",
  228. "reason",
  229. "regex",
  230. "rego",
  231. "renpy",
  232. "rest",
  233. "rip",
  234. "roboconf",
  235. "robotframework",
  236. "ruby",
  237. "rust",
  238. "sas",
  239. "sass",
  240. "scala",
  241. "scheme",
  242. "scss",
  243. "shell-session",
  244. "smali",
  245. "smalltalk",
  246. "smarty",
  247. "sml",
  248. "solidity",
  249. "solution-file",
  250. "soy",
  251. "sparql",
  252. "splunk-spl",
  253. "sqf",
  254. "sql",
  255. "squirrel",
  256. "stan",
  257. "stylus",
  258. "swift",
  259. "systemd",
  260. "t4-cs",
  261. "t4-templating",
  262. "t4-vb",
  263. "tap",
  264. "tcl",
  265. "textile",
  266. "toml",
  267. "tremor",
  268. "tsx",
  269. "tt2",
  270. "turtle",
  271. "twig",
  272. "typescript",
  273. "typoscript",
  274. "unrealscript",
  275. "uorazor",
  276. "uri",
  277. "v",
  278. "vala",
  279. "vbnet",
  280. "velocity",
  281. "verilog",
  282. "vhdl",
  283. "vim",
  284. "visual-basic",
  285. "warpscript",
  286. "wasm",
  287. "web-idl",
  288. "wiki",
  289. "wolfram",
  290. "wren",
  291. "xeora",
  292. "xml-doc",
  293. "xojo",
  294. "xquery",
  295. "yaml",
  296. "yang",
  297. "zig",
  298. ]
  299. def construct_theme_var(theme: str) -> Var[Theme]:
  300. """Construct a theme var.
  301. Args:
  302. theme: The theme to construct.
  303. Returns:
  304. The constructed theme var.
  305. """
  306. return Var(
  307. theme,
  308. _var_data=VarData(
  309. imports={
  310. f"react-syntax-highlighter/dist/cjs/styles/prism/{format.to_kebab_case(theme)}": [
  311. ImportVar(tag=theme, is_default=True, install=False)
  312. ]
  313. }
  314. ),
  315. )
  316. @dataclasses.dataclass(init=False)
  317. class Theme:
  318. """Themes for the CodeBlock component."""
  319. a11y_dark: ClassVar[Var[Theme]] = construct_theme_var("a11yDark")
  320. atom_dark: ClassVar[Var[Theme]] = construct_theme_var("atomDark")
  321. cb: ClassVar[Var[Theme]] = construct_theme_var("cb")
  322. coldark_cold: ClassVar[Var[Theme]] = construct_theme_var("coldarkCold")
  323. coldark_dark: ClassVar[Var[Theme]] = construct_theme_var("coldarkDark")
  324. coy: ClassVar[Var[Theme]] = construct_theme_var("coy")
  325. coy_without_shadows: ClassVar[Var[Theme]] = construct_theme_var("coyWithoutShadows")
  326. darcula: ClassVar[Var[Theme]] = construct_theme_var("darcula")
  327. dark: ClassVar[Var[Theme]] = construct_theme_var("oneDark")
  328. dracula: ClassVar[Var[Theme]] = construct_theme_var("dracula")
  329. duotone_dark: ClassVar[Var[Theme]] = construct_theme_var("duotoneDark")
  330. duotone_earth: ClassVar[Var[Theme]] = construct_theme_var("duotoneEarth")
  331. duotone_forest: ClassVar[Var[Theme]] = construct_theme_var("duotoneForest")
  332. duotone_light: ClassVar[Var[Theme]] = construct_theme_var("duotoneLight")
  333. duotone_sea: ClassVar[Var[Theme]] = construct_theme_var("duotoneSea")
  334. duotone_space: ClassVar[Var[Theme]] = construct_theme_var("duotoneSpace")
  335. funky: ClassVar[Var[Theme]] = construct_theme_var("funky")
  336. ghcolors: ClassVar[Var[Theme]] = construct_theme_var("ghcolors")
  337. gruvbox_dark: ClassVar[Var[Theme]] = construct_theme_var("gruvboxDark")
  338. gruvbox_light: ClassVar[Var[Theme]] = construct_theme_var("gruvboxLight")
  339. holi_theme: ClassVar[Var[Theme]] = construct_theme_var("holiTheme")
  340. hopscotch: ClassVar[Var[Theme]] = construct_theme_var("hopscotch")
  341. light: ClassVar[Var[Theme]] = construct_theme_var("oneLight")
  342. lucario: ClassVar[Var[Theme]] = construct_theme_var("lucario")
  343. material_dark: ClassVar[Var[Theme]] = construct_theme_var("materialDark")
  344. material_light: ClassVar[Var[Theme]] = construct_theme_var("materialLight")
  345. material_oceanic: ClassVar[Var[Theme]] = construct_theme_var("materialOceanic")
  346. night_owl: ClassVar[Var[Theme]] = construct_theme_var("nightOwl")
  347. nord: ClassVar[Var[Theme]] = construct_theme_var("nord")
  348. okaidia: ClassVar[Var[Theme]] = construct_theme_var("okaidia")
  349. one_dark: ClassVar[Var[Theme]] = construct_theme_var("oneDark")
  350. one_light: ClassVar[Var[Theme]] = construct_theme_var("oneLight")
  351. pojoaque: ClassVar[Var[Theme]] = construct_theme_var("pojoaque")
  352. prism: ClassVar[Var[Theme]] = construct_theme_var("prism")
  353. shades_of_purple: ClassVar[Var[Theme]] = construct_theme_var("shadesOfPurple")
  354. solarized_dark_atom: ClassVar[Var[Theme]] = construct_theme_var("solarizedDarkAtom")
  355. solarizedlight: ClassVar[Var[Theme]] = construct_theme_var("solarizedlight")
  356. synthwave84: ClassVar[Var[Theme]] = construct_theme_var("synthwave84")
  357. tomorrow: ClassVar[Var[Theme]] = construct_theme_var("tomorrow")
  358. twilight: ClassVar[Var[Theme]] = construct_theme_var("twilight")
  359. vs: ClassVar[Var[Theme]] = construct_theme_var("vs")
  360. vs_dark: ClassVar[Var[Theme]] = construct_theme_var("vsDark")
  361. vsc_dark_plus: ClassVar[Var[Theme]] = construct_theme_var("vscDarkPlus")
  362. xonokai: ClassVar[Var[Theme]] = construct_theme_var("xonokai")
  363. z_touch: ClassVar[Var[Theme]] = construct_theme_var("zTouch")
  364. for theme_name in dir(Theme):
  365. if theme_name.startswith("_"):
  366. continue
  367. setattr(Theme, theme_name, getattr(Theme, theme_name)._replace(_var_type=Theme))
  368. class CodeBlock(Component, MarkdownComponentMap):
  369. """A code block."""
  370. library = "react-syntax-highlighter@15.6.1"
  371. tag = "PrismAsyncLight"
  372. alias = "SyntaxHighlighter"
  373. # The theme to use ("light" or "dark").
  374. theme: Var[Theme | str] = Theme.one_light
  375. # The language to use.
  376. language: Var[LiteralCodeLanguage] = Var.create("python")
  377. # The code to display.
  378. code: Var[str]
  379. # If this is enabled line numbers will be shown next to the code block.
  380. show_line_numbers: Var[bool]
  381. # The starting line number to use.
  382. starting_line_number: Var[int]
  383. # Whether to wrap long lines.
  384. wrap_long_lines: Var[bool]
  385. # A custom style for the code block.
  386. custom_style: dict[str, str | Var | Color] = {}
  387. # Props passed down to the code tag.
  388. code_tag_props: Var[dict[str, str]]
  389. # Whether a copy button should appear.
  390. can_copy: bool | None = False
  391. # A custom copy button to override the default one.
  392. copy_button: bool | Component | None = None
  393. @classmethod
  394. def create(
  395. cls,
  396. *children,
  397. **props,
  398. ):
  399. """Create a text component.
  400. Args:
  401. *children: The children of the component.
  402. **props: The props to pass to the component.
  403. Returns:
  404. The text component.
  405. """
  406. # This component handles style in a special prop.
  407. custom_style = props.pop("custom_style", {})
  408. can_copy = props.pop("can_copy", False)
  409. copy_button = props.pop("copy_button", None)
  410. # react-syntax-highlighter doesn't have an explicit "light" or "dark" theme so we use one-light and one-dark
  411. # themes respectively to ensure code compatibility.
  412. if "theme" not in props:
  413. # Default color scheme responds to global color mode.
  414. props["theme"] = color_mode_cond(
  415. light=Theme.one_light,
  416. dark=Theme.one_dark,
  417. )
  418. if can_copy:
  419. code = children[0]
  420. copy_button = (
  421. copy_button
  422. if copy_button is not None
  423. else Button.create(
  424. Icon.create(tag="copy"),
  425. on_click=set_clipboard(code),
  426. style=Style({"position": "absolute", "top": "0.5em", "right": "0"}),
  427. )
  428. )
  429. custom_style.update({"padding": "1em 3.2em 1em 1em"})
  430. else:
  431. copy_button = None
  432. # Transfer style props to the custom style prop.
  433. for key, value in props.items():
  434. if key not in cls.get_fields():
  435. custom_style[key] = value
  436. # Carry the children (code) via props
  437. if children:
  438. props["code"] = children[0]
  439. if not isinstance(props["code"], Var):
  440. props["code"] = LiteralVar.create(props["code"])
  441. # Create the component.
  442. code_block = super().create(
  443. **props,
  444. custom_style=Style(custom_style),
  445. )
  446. if copy_button:
  447. return Box.create(code_block, copy_button, position="relative")
  448. else:
  449. return code_block
  450. def add_style(self):
  451. """Add style to the component."""
  452. self.custom_style.update(self.style)
  453. def _render(self):
  454. out = super()._render()
  455. theme = self.theme
  456. out.add_props(style=theme).remove_props("theme", "code").add_props(
  457. children=self.code,
  458. )
  459. return out
  460. def _exclude_props(self) -> list[str]:
  461. return ["can_copy", "copy_button"]
  462. @classmethod
  463. def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> Var:
  464. """Get the hook to register the language.
  465. Args:
  466. language_var: The const/literal Var of the language module to import.
  467. For markdown, uses the default placeholder _LANGUAGE. For direct use,
  468. a LiteralStringVar should be passed via the language prop.
  469. Returns:
  470. The hook to register the language.
  471. """
  472. language_in_there = Var.create(typing.get_args(LiteralCodeLanguage)).contains(
  473. language_var
  474. )
  475. async_load = f"""
  476. (async () => {{
  477. try {{
  478. const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`);
  479. SyntaxHighlighter.registerLanguage({language_var!s}, module.default);
  480. }} catch (error) {{
  481. console.error(`Language ${{{language_var!s}}} is not supported for code blocks inside of markdown: `, error);
  482. }}
  483. }})();
  484. """
  485. return Var(
  486. f"""
  487. if ({language_var!s}) {{
  488. if (!{language_in_there!s}) {{
  489. console.warn(`Language \\`${{{language_var!s}}}\\` is not supported for code blocks inside of markdown.`);
  490. {language_var!s} = '';
  491. }} else {{
  492. {async_load!s}
  493. }}
  494. }}
  495. """
  496. if not isinstance(language_var, LiteralVar)
  497. else f"""
  498. if ({language_var!s}) {{
  499. {async_load!s}
  500. }}""",
  501. _var_data=VarData(
  502. imports={
  503. cls.__fields__["library"].default: [
  504. ImportVar(tag="PrismAsyncLight", alias="SyntaxHighlighter")
  505. ]
  506. },
  507. ),
  508. )
  509. @classmethod
  510. def get_component_map_custom_code(cls) -> Var:
  511. """Get the custom code for the component.
  512. Returns:
  513. The custom code for the component.
  514. """
  515. return cls._get_language_registration_hook()
  516. def add_hooks(self) -> list[str | Var]:
  517. """Add hooks for the component.
  518. Returns:
  519. The hooks for the component.
  520. """
  521. return [
  522. self._get_language_registration_hook(language_var=self.language),
  523. ]
  524. class CodeblockNamespace(ComponentNamespace):
  525. """Namespace for the CodeBlock component."""
  526. themes = Theme
  527. __call__ = CodeBlock.create
  528. code_block = CodeblockNamespace()