code.py 11 KB

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