code.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. """A code component."""
  2. from __future__ import annotations
  3. from typing import Any, Dict, Literal, Optional, Union
  4. from typing_extensions import get_args
  5. from reflex.components.component import Component
  6. from reflex.components.core.cond import color_mode_cond
  7. from reflex.components.lucide.icon import Icon
  8. from reflex.components.radix.themes.components.button import Button
  9. from reflex.components.radix.themes.layout.box import Box
  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
  14. from reflex.utils.imports import ImportDict, ImportVar
  15. from reflex.vars.base import LiteralVar, 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. def replace_quotes_with_camel_case(value: str) -> str:
  345. """Replaces quotes in the given string with camel case format.
  346. Args:
  347. value (str): The string to be processed.
  348. Returns:
  349. str: The processed string with quotes replaced by camel case.
  350. """
  351. for theme in get_args(LiteralCodeBlockTheme):
  352. value = value.replace(f'"{theme}"', format.to_camel_case(theme))
  353. return value
  354. class CodeBlock(Component):
  355. """A code block."""
  356. library = "react-syntax-highlighter@15.5.0"
  357. tag = "PrismAsyncLight"
  358. alias = "SyntaxHighlighter"
  359. # The theme to use ("light" or "dark").
  360. theme: Var[Any] = "one-light" # type: ignore
  361. # The language to use.
  362. language: Var[LiteralCodeLanguage] = "python" # type: ignore
  363. # The code to display.
  364. code: Var[str]
  365. # If this is enabled line numbers will be shown next to the code block.
  366. show_line_numbers: Var[bool]
  367. # The starting line number to use.
  368. starting_line_number: Var[int]
  369. # Whether to wrap long lines.
  370. wrap_long_lines: Var[bool]
  371. # A custom style for the code block.
  372. custom_style: Dict[str, Union[str, Var, Color]] = {}
  373. # Props passed down to the code tag.
  374. code_tag_props: Var[Dict[str, str]]
  375. def add_imports(self) -> ImportDict:
  376. """Add imports for the CodeBlock component.
  377. Returns:
  378. The import dict.
  379. """
  380. imports_: ImportDict = {}
  381. themeString = str(self.theme)
  382. selected_themes = []
  383. for possibleTheme in get_args(LiteralCodeBlockTheme):
  384. if format.to_camel_case(possibleTheme) in themeString:
  385. selected_themes.append(possibleTheme)
  386. if possibleTheme in themeString:
  387. selected_themes.append(possibleTheme)
  388. selected_themes = sorted(set(map(self.convert_theme_name, selected_themes)))
  389. imports_.update(
  390. {
  391. f"react-syntax-highlighter/dist/cjs/styles/prism/{theme}": [
  392. ImportVar(
  393. tag=format.to_camel_case(theme),
  394. is_default=True,
  395. install=False,
  396. )
  397. ]
  398. for theme in selected_themes
  399. }
  400. )
  401. if (
  402. self.language is not None
  403. and (language_without_quotes := str(self.language).replace('"', ""))
  404. in LiteralCodeLanguage.__args__ # type: ignore
  405. ):
  406. imports_[
  407. f"react-syntax-highlighter/dist/cjs/languages/prism/{language_without_quotes}"
  408. ] = [
  409. ImportVar(
  410. tag=format.to_camel_case(language_without_quotes),
  411. is_default=True,
  412. install=False,
  413. )
  414. ]
  415. return imports_
  416. def _get_custom_code(self) -> Optional[str]:
  417. if (
  418. self.language is not None
  419. and (language_without_quotes := str(self.language).replace('"', ""))
  420. in LiteralCodeLanguage.__args__ # type: ignore
  421. ):
  422. return f"{self.alias}.registerLanguage('{language_without_quotes}', {format.to_camel_case(language_without_quotes)})"
  423. @classmethod
  424. def create(
  425. cls,
  426. *children,
  427. can_copy: Optional[bool] = False,
  428. copy_button: Optional[Union[bool, Component]] = None,
  429. **props,
  430. ):
  431. """Create a text component.
  432. Args:
  433. *children: The children of the component.
  434. can_copy: Whether a copy button should appears.
  435. copy_button: A custom copy button to override the default one.
  436. **props: The props to pass to the component.
  437. Returns:
  438. The text component.
  439. """
  440. # This component handles style in a special prop.
  441. custom_style = props.pop("custom_style", {})
  442. if "theme" not in props:
  443. # Default color scheme responds to global color mode.
  444. props["theme"] = color_mode_cond(
  445. light=Var(_js_expr="oneLight"),
  446. dark=Var(_js_expr="oneDark"),
  447. )
  448. # react-syntax-highlighter doesnt have an explicit "light" or "dark" theme so we use one-light and one-dark
  449. # themes respectively to ensure code compatibility.
  450. if "theme" in props and not isinstance(props["theme"], Var):
  451. props["theme"] = cls.convert_theme_name(props["theme"])
  452. if can_copy:
  453. code = children[0]
  454. copy_button = ( # type: ignore
  455. copy_button
  456. if copy_button is not None
  457. else Button.create(
  458. Icon.create(tag="copy"),
  459. on_click=set_clipboard(code),
  460. style=Style({"position": "absolute", "top": "0.5em", "right": "0"}),
  461. )
  462. )
  463. custom_style.update({"padding": "1em 3.2em 1em 1em"})
  464. else:
  465. copy_button = None
  466. # Transfer style props to the custom style prop.
  467. for key, value in props.items():
  468. if key not in cls.get_fields():
  469. custom_style[key] = value
  470. # Carry the children (code) via props
  471. if children:
  472. props["code"] = children[0]
  473. if not isinstance(props["code"], Var):
  474. props["code"] = LiteralVar.create(props["code"])
  475. # Create the component.
  476. code_block = super().create(
  477. **props,
  478. custom_style=Style(custom_style),
  479. )
  480. if copy_button:
  481. return Box.create(code_block, copy_button, position="relative")
  482. else:
  483. return code_block
  484. def add_style(self):
  485. """Add style to the component."""
  486. self.custom_style.update(self.style)
  487. def _render(self):
  488. out = super()._render()
  489. theme = self.theme._replace(
  490. _js_expr=replace_quotes_with_camel_case(str(self.theme))
  491. )
  492. out.add_props(style=theme).remove_props("theme", "code").add_props(
  493. children=self.code
  494. )
  495. return out
  496. @staticmethod
  497. def convert_theme_name(theme) -> str:
  498. """Convert theme names to appropriate names.
  499. Args:
  500. theme: The theme name.
  501. Returns:
  502. The right theme name.
  503. """
  504. if theme in ["light", "dark"]:
  505. return f"one-{theme}"
  506. return theme
  507. code_block = CodeBlock.create