shiki_code_block.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. """Shiki syntax hghlighter component."""
  2. from __future__ import annotations
  3. import dataclasses
  4. import re
  5. from collections import defaultdict
  6. from dataclasses import dataclass
  7. from typing import Any, Literal
  8. from reflex.components.component import Component, ComponentNamespace
  9. from reflex.components.core.colors import color
  10. from reflex.components.core.cond import color_mode_cond
  11. from reflex.components.el.elements.forms import Button
  12. from reflex.components.lucide.icon import Icon
  13. from reflex.components.markdown.markdown import MarkdownComponentMap
  14. from reflex.components.props import NoExtrasAllowedProps
  15. from reflex.components.radix.themes.layout.box import Box
  16. from reflex.event import run_script, set_clipboard
  17. from reflex.style import Style
  18. from reflex.utils.exceptions import VarTypeError
  19. from reflex.utils.imports import ImportVar
  20. from reflex.vars.base import LiteralVar, Var
  21. from reflex.vars.function import FunctionStringVar
  22. from reflex.vars.sequence import StringVar, string_replace_operation
  23. def copy_script() -> Any:
  24. """Copy script for the code block and modify the child SVG element.
  25. Returns:
  26. Any: The result of calling the script.
  27. """
  28. return run_script(
  29. """
  30. // Event listener for the parent click
  31. document.addEventListener('click', function(event) {
  32. // Find the closest button (parent element)
  33. const parent = event.target.closest('button');
  34. // If the parent is found
  35. if (parent) {
  36. // Find the SVG element within the parent
  37. const svgIcon = parent.querySelector('svg');
  38. // If the SVG exists, proceed with the script
  39. if (svgIcon) {
  40. const originalPath = svgIcon.innerHTML;
  41. const checkmarkPath = '<polyline points="20 6 9 17 4 12"></polyline>'; // Checkmark SVG path
  42. function transition(element, scale, opacity) {
  43. element.style.transform = `scale(${scale})`;
  44. element.style.opacity = opacity;
  45. }
  46. // Animate the SVG
  47. transition(svgIcon, 0, '0');
  48. setTimeout(() => {
  49. svgIcon.innerHTML = checkmarkPath; // Replace content with checkmark
  50. svgIcon.setAttribute('viewBox', '0 0 24 24'); // Adjust viewBox if necessary
  51. transition(svgIcon, 1, '1');
  52. setTimeout(() => {
  53. transition(svgIcon, 0, '0');
  54. setTimeout(() => {
  55. svgIcon.innerHTML = originalPath; // Restore original SVG content
  56. transition(svgIcon, 1, '1');
  57. }, 125);
  58. }, 600);
  59. }, 125);
  60. } else {
  61. // console.error('SVG element not found within the parent.');
  62. }
  63. } else {
  64. // console.error('Parent element not found.');
  65. }
  66. })
  67. """
  68. )
  69. SHIKIJS_TRANSFORMER_FNS = {
  70. "transformerNotationDiff",
  71. "transformerNotationHighlight",
  72. "transformerNotationWordHighlight",
  73. "transformerNotationFocus",
  74. "transformerNotationErrorLevel",
  75. "transformerRenderWhitespace",
  76. "transformerMetaHighlight",
  77. "transformerMetaWordHighlight",
  78. "transformerCompactLineOptions",
  79. # TODO: this transformer when included adds a weird behavior which removes other code lines. Need to figure out why.
  80. # "transformerRemoveLineBreak",
  81. "transformerRemoveNotationEscape",
  82. }
  83. LINE_NUMBER_STYLING = {
  84. "code": {
  85. "counter-reset": "step",
  86. "counter-increment": "step 0",
  87. "display": "grid",
  88. "line-height": "1.7",
  89. "font-size": "0.875em",
  90. },
  91. "code .line::before": {
  92. "content": "counter(step)",
  93. "counter-increment": "step",
  94. "width": "1rem",
  95. "margin-right": "1.5rem",
  96. "display": "inline-block",
  97. "text-align": "right",
  98. "color": "rgba(115,138,148,.4)",
  99. },
  100. }
  101. BOX_PARENT_STYLING = {
  102. "pre": {
  103. "margin": "0",
  104. "padding": "24px",
  105. "background": "transparent",
  106. "overflow-x": "auto",
  107. "border-radius": "6px",
  108. },
  109. }
  110. THEME_MAPPING = {
  111. "light": "one-light",
  112. "dark": "one-dark-pro",
  113. "a11y-dark": "github-dark",
  114. }
  115. LANGUAGE_MAPPING = {"bash": "shellscript"}
  116. LiteralCodeLanguage = Literal[
  117. "abap",
  118. "actionscript-3",
  119. "ada",
  120. "angular-html",
  121. "angular-ts",
  122. "apache",
  123. "apex",
  124. "apl",
  125. "applescript",
  126. "ara",
  127. "asciidoc",
  128. "asm",
  129. "astro",
  130. "awk",
  131. "ballerina",
  132. "bat",
  133. "beancount",
  134. "berry",
  135. "bibtex",
  136. "bicep",
  137. "blade",
  138. "c",
  139. "cadence",
  140. "clarity",
  141. "clojure",
  142. "cmake",
  143. "cobol",
  144. "codeowners",
  145. "codeql",
  146. "coffee",
  147. "common-lisp",
  148. "coq",
  149. "cpp",
  150. "crystal",
  151. "csharp",
  152. "css",
  153. "csv",
  154. "cue",
  155. "cypher",
  156. "d",
  157. "dart",
  158. "dax",
  159. "desktop",
  160. "diff",
  161. "docker",
  162. "dotenv",
  163. "dream-maker",
  164. "edge",
  165. "elixir",
  166. "elm",
  167. "emacs-lisp",
  168. "erb",
  169. "erlang",
  170. "fennel",
  171. "fish",
  172. "fluent",
  173. "fortran-fixed-form",
  174. "fortran-free-form",
  175. "fsharp",
  176. "gdresource",
  177. "gdscript",
  178. "gdshader",
  179. "genie",
  180. "gherkin",
  181. "git-commit",
  182. "git-rebase",
  183. "gleam",
  184. "glimmer-js",
  185. "glimmer-ts",
  186. "glsl",
  187. "gnuplot",
  188. "go",
  189. "graphql",
  190. "groovy",
  191. "hack",
  192. "haml",
  193. "handlebars",
  194. "haskell",
  195. "haxe",
  196. "hcl",
  197. "hjson",
  198. "hlsl",
  199. "html",
  200. "html-derivative",
  201. "http",
  202. "hxml",
  203. "hy",
  204. "imba",
  205. "ini",
  206. "java",
  207. "javascript",
  208. "jinja",
  209. "jison",
  210. "json",
  211. "json5",
  212. "jsonc",
  213. "jsonl",
  214. "jsonnet",
  215. "jssm",
  216. "jsx",
  217. "julia",
  218. "kotlin",
  219. "kusto",
  220. "latex",
  221. "lean",
  222. "less",
  223. "liquid",
  224. "log",
  225. "logo",
  226. "lua",
  227. "luau",
  228. "make",
  229. "markdown",
  230. "marko",
  231. "matlab",
  232. "mdc",
  233. "mdx",
  234. "mermaid",
  235. "mojo",
  236. "move",
  237. "narrat",
  238. "nextflow",
  239. "nginx",
  240. "nim",
  241. "nix",
  242. "nushell",
  243. "objective-c",
  244. "objective-cpp",
  245. "ocaml",
  246. "pascal",
  247. "perl",
  248. "php",
  249. "plain",
  250. "plsql",
  251. "po",
  252. "postcss",
  253. "powerquery",
  254. "powershell",
  255. "prisma",
  256. "prolog",
  257. "proto",
  258. "pug",
  259. "puppet",
  260. "purescript",
  261. "python",
  262. "qml",
  263. "qmldir",
  264. "qss",
  265. "r",
  266. "racket",
  267. "raku",
  268. "razor",
  269. "reg",
  270. "regexp",
  271. "rel",
  272. "riscv",
  273. "rst",
  274. "ruby",
  275. "rust",
  276. "sas",
  277. "sass",
  278. "scala",
  279. "scheme",
  280. "scss",
  281. "shaderlab",
  282. "shellscript",
  283. "shellsession",
  284. "smalltalk",
  285. "solidity",
  286. "soy",
  287. "sparql",
  288. "splunk",
  289. "sql",
  290. "ssh-config",
  291. "stata",
  292. "stylus",
  293. "svelte",
  294. "swift",
  295. "system-verilog",
  296. "systemd",
  297. "tasl",
  298. "tcl",
  299. "templ",
  300. "terraform",
  301. "tex",
  302. "toml",
  303. "ts-tags",
  304. "tsv",
  305. "tsx",
  306. "turtle",
  307. "twig",
  308. "typescript",
  309. "typespec",
  310. "typst",
  311. "v",
  312. "vala",
  313. "vb",
  314. "verilog",
  315. "vhdl",
  316. "viml",
  317. "vue",
  318. "vue-html",
  319. "vyper",
  320. "wasm",
  321. "wenyan",
  322. "wgsl",
  323. "wikitext",
  324. "wolfram",
  325. "xml",
  326. "xsl",
  327. "yaml",
  328. "zenscript",
  329. "zig",
  330. ]
  331. LiteralCodeTheme = Literal[
  332. "andromeeda",
  333. "aurora-x",
  334. "ayu-dark",
  335. "catppuccin-frappe",
  336. "catppuccin-latte",
  337. "catppuccin-macchiato",
  338. "catppuccin-mocha",
  339. "dark-plus",
  340. "dracula",
  341. "dracula-soft",
  342. "everforest-dark",
  343. "everforest-light",
  344. "github-dark",
  345. "github-dark-default",
  346. "github-dark-dimmed",
  347. "github-dark-high-contrast",
  348. "github-light",
  349. "github-light-default",
  350. "github-light-high-contrast",
  351. "houston",
  352. "laserwave",
  353. "light-plus",
  354. "material-theme",
  355. "material-theme-darker",
  356. "material-theme-lighter",
  357. "material-theme-ocean",
  358. "material-theme-palenight",
  359. "min-dark",
  360. "min-light",
  361. "monokai",
  362. "night-owl",
  363. "nord",
  364. "one-dark-pro",
  365. "one-light",
  366. "plastic",
  367. "poimandres",
  368. "red",
  369. # rose-pine themes dont work with the current version of shikijs transformers
  370. # https://github.com/shikijs/shiki/issues/730
  371. "rose-pine",
  372. "rose-pine-dawn",
  373. "rose-pine-moon",
  374. "slack-dark",
  375. "slack-ochin",
  376. "snazzy-light",
  377. "solarized-dark",
  378. "solarized-light",
  379. "synthwave-84",
  380. "tokyo-night",
  381. "vesper",
  382. "vitesse-black",
  383. "vitesse-dark",
  384. "vitesse-light",
  385. ]
  386. class Position(NoExtrasAllowedProps):
  387. """Position of the decoration."""
  388. line: int
  389. character: int
  390. class ShikiDecorations(NoExtrasAllowedProps):
  391. """Decorations for the code block."""
  392. start: int | Position
  393. end: int | Position
  394. tag_name: str = "span"
  395. properties: dict[str, Any] = {}
  396. always_wrap: bool = False
  397. @dataclass(kw_only=True)
  398. class ShikiBaseTransformers:
  399. """Base for creating transformers."""
  400. library: str = ""
  401. fns: list[FunctionStringVar] = dataclasses.field(default_factory=list)
  402. style: Style | None = dataclasses.field(default=None)
  403. @dataclass(kw_only=True)
  404. class ShikiJsTransformer(ShikiBaseTransformers):
  405. """A Wrapped shikijs transformer."""
  406. library: str = "@shikijs/transformers@3.3.0"
  407. fns: list[FunctionStringVar] = dataclasses.field(
  408. default_factory=lambda: [
  409. FunctionStringVar.create(fn) for fn in SHIKIJS_TRANSFORMER_FNS
  410. ]
  411. )
  412. style: Style | None = dataclasses.field(
  413. default_factory=lambda: Style(
  414. {
  415. "code": {
  416. "line-height": "1.7",
  417. "font-size": "0.875em",
  418. "display": "grid",
  419. },
  420. # Diffs
  421. ".diff": {
  422. "margin": "0 -24px",
  423. "padding": "0 24px",
  424. "width": "calc(100% + 48px)",
  425. "display": "inline-block",
  426. },
  427. ".diff.add": {
  428. "background-color": "rgba(16, 185, 129, .14)",
  429. "position": "relative",
  430. },
  431. ".diff.remove": {
  432. "background-color": "rgba(244, 63, 94, .14)",
  433. "opacity": "0.7",
  434. "position": "relative",
  435. },
  436. ".diff.remove:after": {
  437. "position": "absolute",
  438. "left": "10px",
  439. "content": "'-'",
  440. "color": "#b34e52",
  441. },
  442. ".diff.add:after": {
  443. "position": "absolute",
  444. "left": "10px",
  445. "content": "'+'",
  446. "color": "#18794e",
  447. },
  448. # Highlight
  449. ".highlighted": {
  450. "background-color": "rgba(142, 150, 170, .14)",
  451. "margin": "0 -24px",
  452. "padding": "0 24px",
  453. "width": "calc(100% + 48px)",
  454. "display": "inline-block",
  455. },
  456. ".highlighted.error": {
  457. "background-color": "rgba(244, 63, 94, .14)",
  458. },
  459. ".highlighted.warning": {
  460. "background-color": "rgba(234, 179, 8, .14)",
  461. },
  462. # Highlighted Word
  463. ".highlighted-word": {
  464. "background-color": color("gray", 2),
  465. "border": f"1px solid {color('gray', 5)}",
  466. "padding": "1px 3px",
  467. "margin": "-1px -3px",
  468. "border-radius": "4px",
  469. },
  470. # Focused Lines
  471. ".has-focused .line:not(.focused)": {
  472. "opacity": "0.7",
  473. "filter": "blur(0.095rem)",
  474. "transition": "filter .35s, opacity .35s",
  475. },
  476. ".has-focused:hover .line:not(.focused)": {
  477. "opacity": "1",
  478. "filter": "none",
  479. },
  480. # White Space
  481. # ".tab, .space": {
  482. # "position": "relative", # noqa: ERA001
  483. # },
  484. # ".tab::before": {
  485. # "content": "'⇥'", # noqa: ERA001
  486. # "position": "absolute", # noqa: ERA001
  487. # "opacity": "0.3",# noqa: ERA001
  488. # },
  489. # ".space::before": {
  490. # "content": "'·'", # noqa: ERA001
  491. # "position": "absolute", # noqa: ERA001
  492. # "opacity": "0.3", # noqa: ERA001
  493. # },
  494. }
  495. )
  496. )
  497. def __init__(self, **kwargs):
  498. """Initialize the transformer.
  499. Args:
  500. kwargs: Kwargs to initialize the props.
  501. """
  502. fns = kwargs.pop("fns", None)
  503. style = kwargs.pop("style", None)
  504. if fns:
  505. kwargs["fns"] = [
  506. (
  507. FunctionStringVar.create(x)
  508. if not isinstance(x, FunctionStringVar)
  509. else x
  510. )
  511. for x in fns
  512. ]
  513. if style:
  514. kwargs["style"] = Style(style)
  515. super().__init__(**kwargs)
  516. class ShikiCodeBlock(Component, MarkdownComponentMap):
  517. """A Code block."""
  518. library = "/components/shiki/code"
  519. tag = "Code"
  520. alias = "ShikiCode"
  521. lib_dependencies: list[str] = ["shiki@3.3.0"]
  522. # The language to use.
  523. language: Var[LiteralCodeLanguage] = Var.create("python")
  524. # The theme to use ("light" or "dark").
  525. theme: Var[LiteralCodeTheme] = Var.create("one-light")
  526. # The set of themes to use for different modes.
  527. themes: Var[list[dict[str, Any]] | dict[str, str]]
  528. # The code to display.
  529. code: Var[str]
  530. # The transformers to use for the syntax highlighter.
  531. transformers: Var[list[ShikiBaseTransformers | dict[str, Any]]] = Var.create([])
  532. # The decorations to use for the syntax highlighter.
  533. decorations: Var[list[ShikiDecorations]] = Var.create([])
  534. @classmethod
  535. def create(
  536. cls,
  537. *children,
  538. **props,
  539. ) -> Component:
  540. """Create a code block component using [shiki syntax highlighter](https://shiki.matsu.io/).
  541. Args:
  542. *children: The children of the component.
  543. **props: The props to pass to the component.
  544. Returns:
  545. The code block component.
  546. """
  547. # Separate props for the code block and the wrapper
  548. code_block_props = {}
  549. code_wrapper_props = {}
  550. decorations = props.pop("decorations", [])
  551. class_props = cls.get_props()
  552. # Distribute props between the code block and wrapper
  553. for key, value in props.items():
  554. (code_block_props if key in class_props else code_wrapper_props)[key] = (
  555. value
  556. )
  557. # cast decorations into ShikiDecorations.
  558. decorations = [
  559. ShikiDecorations(**decoration)
  560. if not isinstance(decoration, ShikiDecorations)
  561. else decoration
  562. for decoration in decorations
  563. ]
  564. code_block_props["decorations"] = decorations
  565. code_block_props["code"] = children[0]
  566. code_block = super().create(**code_block_props)
  567. transformer_styles = {}
  568. # Collect styles from transformers and wrapper
  569. for transformer in code_block.transformers._var_value: # pyright: ignore [reportAttributeAccessIssue]
  570. if isinstance(transformer, ShikiBaseTransformers) and transformer.style:
  571. transformer_styles.update(transformer.style)
  572. transformer_styles.update(code_wrapper_props.pop("style", {}))
  573. return Box.create(
  574. code_block,
  575. *children[1:],
  576. style=Style({**transformer_styles, **BOX_PARENT_STYLING}),
  577. **code_wrapper_props,
  578. )
  579. def add_imports(self) -> dict[str, list[str]]:
  580. """Add the necessary imports.
  581. We add all referenced transformer functions as imports from their corresponding
  582. libraries.
  583. Returns:
  584. Imports for the component.
  585. Raises:
  586. ValueError: If the transformers are not of type LiteralVar.
  587. """
  588. imports = defaultdict(list)
  589. if not isinstance(self.transformers, LiteralVar):
  590. msg = f"transformers should be a LiteralVar type. Got {type(self.transformers)} instead."
  591. raise ValueError(msg)
  592. for transformer in self.transformers._var_value:
  593. if isinstance(transformer, ShikiBaseTransformers):
  594. imports[transformer.library].extend(
  595. [ImportVar(tag=str(fn)) for fn in transformer.fns]
  596. )
  597. if transformer.library not in self.lib_dependencies:
  598. self.lib_dependencies.append(transformer.library)
  599. return imports
  600. @classmethod
  601. def create_transformer(cls, library: str, fns: list[str]) -> ShikiBaseTransformers:
  602. """Create a transformer from a third party library.
  603. Args:
  604. library: The name of the library.
  605. fns: The str names of the functions/callables to invoke from the library.
  606. Returns:
  607. A transformer for the specified library.
  608. Raises:
  609. ValueError: If a supplied function name is not valid str.
  610. """
  611. if any(not isinstance(fn_name, str) for fn_name in fns):
  612. msg = f"the function names should be str names of functions in the specified transformer: {library!r}"
  613. raise ValueError(msg)
  614. return ShikiBaseTransformers(
  615. library=library,
  616. fns=[FunctionStringVar.create(fn) for fn in fns], # pyright: ignore [reportCallIssue]
  617. )
  618. def _render(self, props: dict[str, Any] | None = None):
  619. """Renders the component with the given properties, processing transformers if present.
  620. Args:
  621. props: Optional properties to pass to the render function.
  622. Returns:
  623. Rendered component output.
  624. """
  625. # Ensure props is initialized from class attributes if not provided
  626. props = props or {
  627. attr.rstrip("_"): getattr(self, attr) for attr in self.get_props()
  628. }
  629. # Extract transformers and apply transformations
  630. transformers = props.get("transformers")
  631. if transformers is not None:
  632. transformed_values = self._process_transformers(transformers._var_value)
  633. props["transformers"] = LiteralVar.create(transformed_values)
  634. return super()._render(props)
  635. def _process_transformers(self, transformer_list: list) -> list:
  636. """Processes a list of transformers, applying transformations where necessary.
  637. Args:
  638. transformer_list: List of transformer objects or values.
  639. Returns:
  640. list: A list of transformed values.
  641. """
  642. processed = []
  643. for transformer in transformer_list:
  644. if isinstance(transformer, ShikiBaseTransformers):
  645. processed.extend(fn.call() for fn in transformer.fns)
  646. else:
  647. processed.append(transformer)
  648. return processed
  649. class ShikiHighLevelCodeBlock(ShikiCodeBlock):
  650. """High level component for the shiki syntax highlighter."""
  651. # If this is enabled, the default transformers(shikijs transformer) will be used.
  652. use_transformers: Var[bool]
  653. # If this is enabled line numbers will be shown next to the code block.
  654. show_line_numbers: Var[bool]
  655. # Whether a copy button should appear.
  656. can_copy: bool = False
  657. # copy_button: A custom copy button to override the default one.
  658. copy_button: Component | bool | None = None
  659. @classmethod
  660. def create(
  661. cls,
  662. *children,
  663. **props,
  664. ) -> Component:
  665. """Create a code block component using [shiki syntax highlighter](https://shiki.matsu.io/).
  666. Args:
  667. *children: The children of the component.
  668. **props: The props to pass to the component.
  669. Returns:
  670. The code block component.
  671. """
  672. use_transformers = props.pop("use_transformers", False)
  673. show_line_numbers = props.pop("show_line_numbers", False)
  674. language = props.pop("language", None)
  675. can_copy = props.pop("can_copy", False)
  676. copy_button = props.pop("copy_button", None)
  677. if use_transformers:
  678. props["transformers"] = [ShikiJsTransformer()]
  679. if language is not None:
  680. props["language"] = cls._map_languages(language)
  681. # line numbers are generated via css
  682. if show_line_numbers:
  683. props["style"] = {**LINE_NUMBER_STYLING, **props.get("style", {})}
  684. theme = props.pop("theme", None)
  685. props["theme"] = props["theme"] = (
  686. cls._map_themes(theme)
  687. if theme is not None
  688. else color_mode_cond( # Default color scheme responds to global color mode.
  689. light="one-light",
  690. dark="one-dark-pro",
  691. )
  692. )
  693. if can_copy:
  694. code = children[0]
  695. copy_button = (
  696. copy_button
  697. if copy_button is not None
  698. else Button.create(
  699. Icon.create(tag="copy", size=16, color=color("gray", 11)),
  700. on_click=[
  701. set_clipboard(cls._strip_transformer_triggers(code)),
  702. copy_script(),
  703. ],
  704. style=Style(
  705. {
  706. "position": "absolute",
  707. "top": "4px",
  708. "right": "4px",
  709. "background": color("gray", 3),
  710. "border": "1px solid",
  711. "border-color": color("gray", 5),
  712. "border-radius": "6px",
  713. "padding": "5px",
  714. "opacity": "1",
  715. "cursor": "pointer",
  716. "_hover": {
  717. "background": color("gray", 4),
  718. },
  719. "transition": "background 0.250s ease-out",
  720. "&>svg": {
  721. "transition": "transform 0.250s ease-out, opacity 0.250s ease-out",
  722. },
  723. "_active": {
  724. "background": color("gray", 5),
  725. },
  726. }
  727. ),
  728. )
  729. )
  730. if copy_button:
  731. return ShikiCodeBlock.create(
  732. children[0], copy_button, position="relative", **props
  733. )
  734. return ShikiCodeBlock.create(children[0], **props)
  735. @staticmethod
  736. def _map_themes(theme: str) -> str:
  737. if isinstance(theme, str) and theme in THEME_MAPPING:
  738. return THEME_MAPPING[theme]
  739. return theme
  740. @staticmethod
  741. def _map_languages(language: str) -> str:
  742. if isinstance(language, str) and language in LANGUAGE_MAPPING:
  743. return LANGUAGE_MAPPING[language]
  744. return language
  745. @staticmethod
  746. def _strip_transformer_triggers(code: str | StringVar) -> StringVar | str:
  747. if not isinstance(code, (StringVar, str)):
  748. msg = f"code should be string literal or a StringVar type. Got {type(code)} instead."
  749. raise VarTypeError(msg)
  750. regex_pattern = r"[\/#]+ *\[!code.*?\]"
  751. if isinstance(code, Var):
  752. return string_replace_operation(
  753. code, StringVar(_js_expr=f"/{regex_pattern}/g", _var_type=str), ""
  754. )
  755. if isinstance(code, str):
  756. return re.sub(regex_pattern, "", code)
  757. return None
  758. class TransformerNamespace(ComponentNamespace):
  759. """Namespace for the Transformers."""
  760. shikijs = ShikiJsTransformer
  761. class CodeblockNamespace(ComponentNamespace):
  762. """Namespace for the CodeBlock component."""
  763. root = staticmethod(ShikiCodeBlock.create)
  764. create_transformer = staticmethod(ShikiCodeBlock.create_transformer)
  765. transformers = TransformerNamespace()
  766. __call__ = staticmethod(ShikiHighLevelCodeBlock.create)
  767. code_block = CodeblockNamespace()