shiki_code_block.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. """Shiki syntax hghlighter component."""
  2. from __future__ import annotations
  3. from collections import defaultdict
  4. from typing import Any, Literal, Optional, Union
  5. from reflex.base import Base
  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.radix.themes.components.button import Button
  10. from reflex.components.radix.themes.layout.box import Box
  11. from reflex.event import set_clipboard
  12. from reflex.style import Style
  13. from reflex.utils.imports import ImportVar
  14. from reflex.vars.base import LiteralVar, Var
  15. from reflex.vars.function import FunctionStringVar
  16. SHIKIJS_TRANSFORMER_FNS = {
  17. "transformerNotationDiff",
  18. "transformerNotationHighlight",
  19. "transformerNotationWordHighlight",
  20. "transformerNotationFocus",
  21. "transformerNotationErrorLevel",
  22. "transformerRenderWhitespace",
  23. "transformerMetaHighlight",
  24. "transformerMetaWordHighlight",
  25. "transformerCompactLineOptions",
  26. # TODO: this transformer when included adds a weird behavior which removes other code lines. Need to figure out why.
  27. # "transformerRemoveLineBreak",
  28. "transformerRemoveNotationEscape",
  29. }
  30. LINE_NUMBER_STYLING = {
  31. "code": {"counter-reset": "step", "counter-increment": "step 0"},
  32. "code .line::before": {
  33. "content": "counter(step)",
  34. "counter-increment": "step",
  35. "width": "1rem",
  36. "margin-right": "1.5rem",
  37. "display": "inline-block",
  38. "text-align": "right",
  39. "color": "rgba(115,138,148,.4)",
  40. },
  41. }
  42. THEME_MAPPING = {
  43. "light": "one-light",
  44. "dark": "one-dark-pro",
  45. "a11y-dark": "github-dark",
  46. }
  47. LANGUAGE_MAPPING = {"bash": "shellscript"}
  48. LiteralCodeLanguage = Literal[
  49. "abap",
  50. "actionscript-3",
  51. "ada",
  52. "angular-html",
  53. "angular-ts",
  54. "apache",
  55. "apex",
  56. "apl",
  57. "applescript",
  58. "ara",
  59. "asciidoc",
  60. "asm",
  61. "astro",
  62. "awk",
  63. "ballerina",
  64. "bat",
  65. "beancount",
  66. "berry",
  67. "bibtex",
  68. "bicep",
  69. "blade",
  70. "c",
  71. "cadence",
  72. "clarity",
  73. "clojure",
  74. "cmake",
  75. "cobol",
  76. "codeowners",
  77. "codeql",
  78. "coffee",
  79. "common-lisp",
  80. "coq",
  81. "cpp",
  82. "crystal",
  83. "csharp",
  84. "css",
  85. "csv",
  86. "cue",
  87. "cypher",
  88. "d",
  89. "dart",
  90. "dax",
  91. "desktop",
  92. "diff",
  93. "docker",
  94. "dotenv",
  95. "dream-maker",
  96. "edge",
  97. "elixir",
  98. "elm",
  99. "emacs-lisp",
  100. "erb",
  101. "erlang",
  102. "fennel",
  103. "fish",
  104. "fluent",
  105. "fortran-fixed-form",
  106. "fortran-free-form",
  107. "fsharp",
  108. "gdresource",
  109. "gdscript",
  110. "gdshader",
  111. "genie",
  112. "gherkin",
  113. "git-commit",
  114. "git-rebase",
  115. "gleam",
  116. "glimmer-js",
  117. "glimmer-ts",
  118. "glsl",
  119. "gnuplot",
  120. "go",
  121. "graphql",
  122. "groovy",
  123. "hack",
  124. "haml",
  125. "handlebars",
  126. "haskell",
  127. "haxe",
  128. "hcl",
  129. "hjson",
  130. "hlsl",
  131. "html",
  132. "html-derivative",
  133. "http",
  134. "hxml",
  135. "hy",
  136. "imba",
  137. "ini",
  138. "java",
  139. "javascript",
  140. "jinja",
  141. "jison",
  142. "json",
  143. "json5",
  144. "jsonc",
  145. "jsonl",
  146. "jsonnet",
  147. "jssm",
  148. "jsx",
  149. "julia",
  150. "kotlin",
  151. "kusto",
  152. "latex",
  153. "lean",
  154. "less",
  155. "liquid",
  156. "log",
  157. "logo",
  158. "lua",
  159. "luau",
  160. "make",
  161. "markdown",
  162. "marko",
  163. "matlab",
  164. "mdc",
  165. "mdx",
  166. "mermaid",
  167. "mojo",
  168. "move",
  169. "narrat",
  170. "nextflow",
  171. "nginx",
  172. "nim",
  173. "nix",
  174. "nushell",
  175. "objective-c",
  176. "objective-cpp",
  177. "ocaml",
  178. "pascal",
  179. "perl",
  180. "php",
  181. "plsql",
  182. "po",
  183. "postcss",
  184. "powerquery",
  185. "powershell",
  186. "prisma",
  187. "prolog",
  188. "proto",
  189. "pug",
  190. "puppet",
  191. "purescript",
  192. "python",
  193. "qml",
  194. "qmldir",
  195. "qss",
  196. "r",
  197. "racket",
  198. "raku",
  199. "razor",
  200. "reg",
  201. "regexp",
  202. "rel",
  203. "riscv",
  204. "rst",
  205. "ruby",
  206. "rust",
  207. "sas",
  208. "sass",
  209. "scala",
  210. "scheme",
  211. "scss",
  212. "shaderlab",
  213. "shellscript",
  214. "shellsession",
  215. "smalltalk",
  216. "solidity",
  217. "soy",
  218. "sparql",
  219. "splunk",
  220. "sql",
  221. "ssh-config",
  222. "stata",
  223. "stylus",
  224. "svelte",
  225. "swift",
  226. "system-verilog",
  227. "systemd",
  228. "tasl",
  229. "tcl",
  230. "templ",
  231. "terraform",
  232. "tex",
  233. "toml",
  234. "ts-tags",
  235. "tsv",
  236. "tsx",
  237. "turtle",
  238. "twig",
  239. "typescript",
  240. "typespec",
  241. "typst",
  242. "v",
  243. "vala",
  244. "vb",
  245. "verilog",
  246. "vhdl",
  247. "viml",
  248. "vue",
  249. "vue-html",
  250. "vyper",
  251. "wasm",
  252. "wenyan",
  253. "wgsl",
  254. "wikitext",
  255. "wolfram",
  256. "xml",
  257. "xsl",
  258. "yaml",
  259. "zenscript",
  260. "zig",
  261. ]
  262. LiteralCodeTheme = Literal[
  263. "andromeeda",
  264. "aurora-x",
  265. "ayu-dark",
  266. "catppuccin-frappe",
  267. "catppuccin-latte",
  268. "catppuccin-macchiato",
  269. "catppuccin-mocha",
  270. "dark-plus",
  271. "dracula",
  272. "dracula-soft",
  273. "everforest-dark",
  274. "everforest-light",
  275. "github-dark",
  276. "github-dark-default",
  277. "github-dark-dimmed",
  278. "github-dark-high-contrast",
  279. "github-light",
  280. "github-light-default",
  281. "github-light-high-contrast",
  282. "houston",
  283. "laserwave",
  284. "light-plus",
  285. "material-theme",
  286. "material-theme-darker",
  287. "material-theme-lighter",
  288. "material-theme-ocean",
  289. "material-theme-palenight",
  290. "min-dark",
  291. "min-light",
  292. "monokai",
  293. "night-owl",
  294. "nord",
  295. "one-dark-pro",
  296. "one-light",
  297. "plastic",
  298. "poimandres",
  299. "red",
  300. "rose-pine",
  301. "rose-pine-dawn",
  302. "rose-pine-moon",
  303. "slack-dark",
  304. "slack-ochin",
  305. "snazzy-light",
  306. "solarized-dark",
  307. "solarized-light",
  308. "synthwave-84",
  309. "tokyo-night",
  310. "vesper",
  311. "vitesse-black",
  312. "vitesse-dark",
  313. "vitesse-light",
  314. ]
  315. class ShikiBaseTransformers(Base):
  316. """Base for creating transformers."""
  317. library: str
  318. fns: list[FunctionStringVar]
  319. style: Optional[Style]
  320. class ShikiJsTransformer(ShikiBaseTransformers):
  321. """A Wrapped shikijs transformer."""
  322. library: str = "@shikijs/transformers"
  323. fns: list[FunctionStringVar] = [
  324. FunctionStringVar.create(fn) for fn in SHIKIJS_TRANSFORMER_FNS
  325. ]
  326. style: Optional[Style] = Style(
  327. {
  328. ".line": {"display": "inline", "padding-bottom": "0"},
  329. ".diff": {
  330. "display": "inline-block",
  331. "width": "100vw",
  332. "margin": "0 -12px",
  333. "padding": "0 12px",
  334. },
  335. ".diff.add": {"background-color": "#0505"},
  336. ".diff.remove": {"background-color": "#8005"},
  337. ".diff:before": {"position": "absolute", "left": "40px"},
  338. ".has-focused .line": {"filter": "blur(0.095rem)"},
  339. ".has-focused .focused": {"filter": "blur(0)"},
  340. }
  341. )
  342. def __init__(self, **kwargs):
  343. """Initialize the transformer.
  344. Args:
  345. kwargs: Kwargs to initialize the props.
  346. """
  347. fns = kwargs.pop("fns", None)
  348. style = kwargs.pop("style", None)
  349. if fns:
  350. kwargs["fns"] = [
  351. FunctionStringVar.create(x)
  352. if not isinstance(x, FunctionStringVar)
  353. else x
  354. for x in fns
  355. ]
  356. if style:
  357. kwargs["style"] = Style(style)
  358. super().__init__(**kwargs)
  359. class ShikiCodeBlock(Component):
  360. """A Code block."""
  361. library = "/components/shiki/code"
  362. tag = "Code"
  363. alias = "ShikiCode"
  364. lib_dependencies: list[str] = ["shiki"]
  365. # The language to use.
  366. language: Var[LiteralCodeLanguage] = Var.create("python")
  367. # The theme to use ("light" or "dark").
  368. theme: Var[LiteralCodeTheme] = Var.create("one-light")
  369. # The set of themes to use for different modes.
  370. themes: Var[Union[list[dict[str, Any]], dict[str, str]]]
  371. # The code to display.
  372. code: Var[str]
  373. # The transformers to use for the syntax highlighter.
  374. transformers: Var[list[Union[ShikiBaseTransformers, dict[str, Any]]]] = Var.create(
  375. []
  376. )
  377. @classmethod
  378. def create(
  379. cls,
  380. *children,
  381. **props,
  382. ) -> Component:
  383. """Create a code block component using [shiki syntax highlighter](https://shiki.matsu.io/).
  384. Args:
  385. *children: The children of the component.
  386. **props: The props to pass to the component.
  387. Returns:
  388. The code block component.
  389. """
  390. # Separate props for the code block and the wrapper
  391. code_block_props = {}
  392. code_wrapper_props = {}
  393. class_props = cls.get_props()
  394. # Distribute props between the code block and wrapper
  395. for key, value in props.items():
  396. (code_block_props if key in class_props else code_wrapper_props)[key] = (
  397. value
  398. )
  399. code_block_props["code"] = children[0]
  400. code_block = super().create(**code_block_props)
  401. transformer_styles = {}
  402. # Collect styles from transformers and wrapper
  403. for transformer in code_block.transformers._var_value: # type: ignore
  404. if isinstance(transformer, ShikiBaseTransformers) and transformer.style:
  405. transformer_styles.update(transformer.style)
  406. transformer_styles.update(code_wrapper_props.pop("style", {}))
  407. return Box.create(
  408. code_block,
  409. *children[1:],
  410. style=Style(transformer_styles),
  411. **code_wrapper_props,
  412. )
  413. def add_imports(self) -> dict[str, list[str]]:
  414. """Add the necessary imports.
  415. We add all referenced transformer functions as imports from their corresponding
  416. libraries.
  417. Returns:
  418. Imports for the component.
  419. """
  420. imports = defaultdict(list)
  421. for transformer in self.transformers._var_value:
  422. if isinstance(transformer, ShikiBaseTransformers):
  423. imports[transformer.library].extend(
  424. [ImportVar(tag=str(fn)) for fn in transformer.fns]
  425. )
  426. self.lib_dependencies.append(
  427. transformer.library
  428. ) if transformer.library not in self.lib_dependencies else None
  429. return imports
  430. @classmethod
  431. def create_transformer(cls, library: str, fns: list[str]) -> ShikiBaseTransformers:
  432. """Create a transformer from a third party library.
  433. Args:
  434. library: The name of the library.
  435. fns: The str names of the functions/callables to invoke from the library.
  436. Returns:
  437. A transformer for the specified library.
  438. Raises:
  439. ValueError: If a supplied function name is not valid str.
  440. """
  441. if any(not isinstance(fn_name, str) for fn_name in fns):
  442. raise ValueError(
  443. f"the function names should be str names of functions in the specified transformer: {library!r}"
  444. )
  445. return ShikiBaseTransformers( # type: ignore
  446. library=library, fns=[FunctionStringVar.create(fn) for fn in fns]
  447. )
  448. def _render(self, props: dict[str, Any] | None = None):
  449. """Renders the component with the given properties, processing transformers if present.
  450. Args:
  451. props: Optional properties to pass to the render function.
  452. Returns:
  453. Rendered component output.
  454. """
  455. # Ensure props is initialized from class attributes if not provided
  456. props = props or {
  457. attr.rstrip("_"): getattr(self, attr) for attr in self.get_props()
  458. }
  459. # Extract transformers and apply transformations
  460. transformers = props.get("transformers")
  461. if transformers is not None:
  462. transformed_values = self._process_transformers(transformers._var_value)
  463. props["transformers"] = LiteralVar.create(transformed_values)
  464. return super()._render(props)
  465. def _process_transformers(self, transformer_list: list) -> list:
  466. """Processes a list of transformers, applying transformations where necessary.
  467. Args:
  468. transformer_list: List of transformer objects or values.
  469. Returns:
  470. list: A list of transformed values.
  471. """
  472. processed = []
  473. for transformer in transformer_list:
  474. if isinstance(transformer, ShikiBaseTransformers):
  475. processed.extend(fn.call() for fn in transformer.fns)
  476. else:
  477. processed.append(transformer)
  478. return processed
  479. class ShikiHighLevelCodeBlock(ShikiCodeBlock):
  480. """High level component for the shiki syntax highlighter."""
  481. # If this is enabled, the default transformers(shikijs transformer) will be used.
  482. use_transformers: Var[bool]
  483. # If this is enabled line numbers will be shown next to the code block.
  484. show_line_numbers: Var[bool]
  485. @classmethod
  486. def create(
  487. cls,
  488. *children,
  489. can_copy: bool | None = False,
  490. copy_button: bool | Component | None = None,
  491. **props,
  492. ) -> Component:
  493. """Create a code block component using [shiki syntax highlighter](https://shiki.matsu.io/).
  494. Args:
  495. *children: The children of the component.
  496. can_copy: Whether a copy button should appear.
  497. copy_button: A custom copy button to override the default one.
  498. **props: The props to pass to the component.
  499. Returns:
  500. The code block component.
  501. """
  502. use_transformers = props.pop("use_transformers", False)
  503. show_line_numbers = props.pop("show_line_numbers", False)
  504. language = props.pop("language", None)
  505. if use_transformers:
  506. props["transformers"] = [ShikiJsTransformer()]
  507. if language is not None:
  508. props["language"] = cls._map_languages(language)
  509. # line numbers are generated via css
  510. if show_line_numbers:
  511. props["style"] = {**LINE_NUMBER_STYLING, **props.get("style", {})}
  512. theme = props.pop("theme", None)
  513. props["theme"] = props["theme"] = (
  514. cls._map_themes(theme)
  515. if theme
  516. else color_mode_cond( # Default color scheme responds to global color mode.
  517. light="one-light",
  518. dark="one-dark-pro",
  519. )
  520. )
  521. if can_copy:
  522. code = children[0]
  523. copy_button = ( # type: ignore
  524. copy_button
  525. if copy_button is not None
  526. else Button.create(
  527. Icon.create(tag="copy"),
  528. on_click=set_clipboard(code),
  529. style=Style({"position": "absolute", "top": "0.5em", "right": "0"}),
  530. )
  531. )
  532. else:
  533. copy_button = None
  534. if copy_button:
  535. return ShikiCodeBlock.create(
  536. children[0], copy_button, position="relative", **props
  537. )
  538. else:
  539. return ShikiCodeBlock.create(children[0], **props)
  540. @staticmethod
  541. def _map_themes(theme: str) -> str:
  542. if isinstance(theme, str) and theme in THEME_MAPPING:
  543. return THEME_MAPPING[theme]
  544. return theme
  545. @staticmethod
  546. def _map_languages(language: str) -> str:
  547. if isinstance(language, str) and language in LANGUAGE_MAPPING:
  548. return LANGUAGE_MAPPING[language]
  549. return language
  550. class TransformerNamespace(ComponentNamespace):
  551. """Namespace for the Transformers."""
  552. shikijs = ShikiJsTransformer
  553. class CodeblockNamespace(ComponentNamespace):
  554. """Namespace for the CodeBlock component."""
  555. root = staticmethod(ShikiCodeBlock.create)
  556. create_transformer = staticmethod(ShikiCodeBlock.create_transformer)
  557. transformers = TransformerNamespace()
  558. __call__ = staticmethod(ShikiHighLevelCodeBlock.create)
  559. code_block = CodeblockNamespace()