code.py 12 KB

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