123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849 |
- """Shiki syntax hghlighter component."""
- from __future__ import annotations
- import re
- from collections import defaultdict
- from typing import Any, Literal
- from reflex.base import Base
- from reflex.components.component import Component, ComponentNamespace
- from reflex.components.core.colors import color
- from reflex.components.core.cond import color_mode_cond
- from reflex.components.el.elements.forms import Button
- from reflex.components.lucide.icon import Icon
- from reflex.components.markdown.markdown import MarkdownComponentMap
- from reflex.components.props import NoExtrasAllowedProps
- from reflex.components.radix.themes.layout.box import Box
- from reflex.event import run_script, set_clipboard
- from reflex.style import Style
- from reflex.utils.exceptions import VarTypeError
- from reflex.utils.imports import ImportVar
- from reflex.vars.base import LiteralVar, Var
- from reflex.vars.function import FunctionStringVar
- from reflex.vars.sequence import StringVar, string_replace_operation
- def copy_script() -> Any:
- """Copy script for the code block and modify the child SVG element.
- Returns:
- Any: The result of calling the script.
- """
- return run_script(
- """
- // Event listener for the parent click
- document.addEventListener('click', function(event) {
- // Find the closest button (parent element)
- const parent = event.target.closest('button');
- // If the parent is found
- if (parent) {
- // Find the SVG element within the parent
- const svgIcon = parent.querySelector('svg');
- // If the SVG exists, proceed with the script
- if (svgIcon) {
- const originalPath = svgIcon.innerHTML;
- const checkmarkPath = '<polyline points="20 6 9 17 4 12"></polyline>'; // Checkmark SVG path
- function transition(element, scale, opacity) {
- element.style.transform = `scale(${scale})`;
- element.style.opacity = opacity;
- }
- // Animate the SVG
- transition(svgIcon, 0, '0');
- setTimeout(() => {
- svgIcon.innerHTML = checkmarkPath; // Replace content with checkmark
- svgIcon.setAttribute('viewBox', '0 0 24 24'); // Adjust viewBox if necessary
- transition(svgIcon, 1, '1');
- setTimeout(() => {
- transition(svgIcon, 0, '0');
- setTimeout(() => {
- svgIcon.innerHTML = originalPath; // Restore original SVG content
- transition(svgIcon, 1, '1');
- }, 125);
- }, 600);
- }, 125);
- } else {
- // console.error('SVG element not found within the parent.');
- }
- } else {
- // console.error('Parent element not found.');
- }
- })
- """
- )
- SHIKIJS_TRANSFORMER_FNS = {
- "transformerNotationDiff",
- "transformerNotationHighlight",
- "transformerNotationWordHighlight",
- "transformerNotationFocus",
- "transformerNotationErrorLevel",
- "transformerRenderWhitespace",
- "transformerMetaHighlight",
- "transformerMetaWordHighlight",
- "transformerCompactLineOptions",
- # TODO: this transformer when included adds a weird behavior which removes other code lines. Need to figure out why.
- # "transformerRemoveLineBreak",
- "transformerRemoveNotationEscape",
- }
- LINE_NUMBER_STYLING = {
- "code": {
- "counter-reset": "step",
- "counter-increment": "step 0",
- "display": "grid",
- "line-height": "1.7",
- "font-size": "0.875em",
- },
- "code .line::before": {
- "content": "counter(step)",
- "counter-increment": "step",
- "width": "1rem",
- "margin-right": "1.5rem",
- "display": "inline-block",
- "text-align": "right",
- "color": "rgba(115,138,148,.4)",
- },
- }
- BOX_PARENT_STYLING = {
- "pre": {
- "margin": "0",
- "padding": "24px",
- "background": "transparent",
- "overflow-x": "auto",
- "border-radius": "6px",
- },
- }
- THEME_MAPPING = {
- "light": "one-light",
- "dark": "one-dark-pro",
- "a11y-dark": "github-dark",
- }
- LANGUAGE_MAPPING = {"bash": "shellscript"}
- LiteralCodeLanguage = Literal[
- "abap",
- "actionscript-3",
- "ada",
- "angular-html",
- "angular-ts",
- "apache",
- "apex",
- "apl",
- "applescript",
- "ara",
- "asciidoc",
- "asm",
- "astro",
- "awk",
- "ballerina",
- "bat",
- "beancount",
- "berry",
- "bibtex",
- "bicep",
- "blade",
- "c",
- "cadence",
- "clarity",
- "clojure",
- "cmake",
- "cobol",
- "codeowners",
- "codeql",
- "coffee",
- "common-lisp",
- "coq",
- "cpp",
- "crystal",
- "csharp",
- "css",
- "csv",
- "cue",
- "cypher",
- "d",
- "dart",
- "dax",
- "desktop",
- "diff",
- "docker",
- "dotenv",
- "dream-maker",
- "edge",
- "elixir",
- "elm",
- "emacs-lisp",
- "erb",
- "erlang",
- "fennel",
- "fish",
- "fluent",
- "fortran-fixed-form",
- "fortran-free-form",
- "fsharp",
- "gdresource",
- "gdscript",
- "gdshader",
- "genie",
- "gherkin",
- "git-commit",
- "git-rebase",
- "gleam",
- "glimmer-js",
- "glimmer-ts",
- "glsl",
- "gnuplot",
- "go",
- "graphql",
- "groovy",
- "hack",
- "haml",
- "handlebars",
- "haskell",
- "haxe",
- "hcl",
- "hjson",
- "hlsl",
- "html",
- "html-derivative",
- "http",
- "hxml",
- "hy",
- "imba",
- "ini",
- "java",
- "javascript",
- "jinja",
- "jison",
- "json",
- "json5",
- "jsonc",
- "jsonl",
- "jsonnet",
- "jssm",
- "jsx",
- "julia",
- "kotlin",
- "kusto",
- "latex",
- "lean",
- "less",
- "liquid",
- "log",
- "logo",
- "lua",
- "luau",
- "make",
- "markdown",
- "marko",
- "matlab",
- "mdc",
- "mdx",
- "mermaid",
- "mojo",
- "move",
- "narrat",
- "nextflow",
- "nginx",
- "nim",
- "nix",
- "nushell",
- "objective-c",
- "objective-cpp",
- "ocaml",
- "pascal",
- "perl",
- "php",
- "plain",
- "plsql",
- "po",
- "postcss",
- "powerquery",
- "powershell",
- "prisma",
- "prolog",
- "proto",
- "pug",
- "puppet",
- "purescript",
- "python",
- "qml",
- "qmldir",
- "qss",
- "r",
- "racket",
- "raku",
- "razor",
- "reg",
- "regexp",
- "rel",
- "riscv",
- "rst",
- "ruby",
- "rust",
- "sas",
- "sass",
- "scala",
- "scheme",
- "scss",
- "shaderlab",
- "shellscript",
- "shellsession",
- "smalltalk",
- "solidity",
- "soy",
- "sparql",
- "splunk",
- "sql",
- "ssh-config",
- "stata",
- "stylus",
- "svelte",
- "swift",
- "system-verilog",
- "systemd",
- "tasl",
- "tcl",
- "templ",
- "terraform",
- "tex",
- "toml",
- "ts-tags",
- "tsv",
- "tsx",
- "turtle",
- "twig",
- "typescript",
- "typespec",
- "typst",
- "v",
- "vala",
- "vb",
- "verilog",
- "vhdl",
- "viml",
- "vue",
- "vue-html",
- "vyper",
- "wasm",
- "wenyan",
- "wgsl",
- "wikitext",
- "wolfram",
- "xml",
- "xsl",
- "yaml",
- "zenscript",
- "zig",
- ]
- LiteralCodeTheme = Literal[
- "andromeeda",
- "aurora-x",
- "ayu-dark",
- "catppuccin-frappe",
- "catppuccin-latte",
- "catppuccin-macchiato",
- "catppuccin-mocha",
- "dark-plus",
- "dracula",
- "dracula-soft",
- "everforest-dark",
- "everforest-light",
- "github-dark",
- "github-dark-default",
- "github-dark-dimmed",
- "github-dark-high-contrast",
- "github-light",
- "github-light-default",
- "github-light-high-contrast",
- "houston",
- "laserwave",
- "light-plus",
- "material-theme",
- "material-theme-darker",
- "material-theme-lighter",
- "material-theme-ocean",
- "material-theme-palenight",
- "min-dark",
- "min-light",
- "monokai",
- "night-owl",
- "nord",
- "one-dark-pro",
- "one-light",
- "plastic",
- "poimandres",
- "red",
- # rose-pine themes dont work with the current version of shikijs transformers
- # https://github.com/shikijs/shiki/issues/730
- "rose-pine",
- "rose-pine-dawn",
- "rose-pine-moon",
- "slack-dark",
- "slack-ochin",
- "snazzy-light",
- "solarized-dark",
- "solarized-light",
- "synthwave-84",
- "tokyo-night",
- "vesper",
- "vitesse-black",
- "vitesse-dark",
- "vitesse-light",
- ]
- class Position(NoExtrasAllowedProps):
- """Position of the decoration."""
- line: int
- character: int
- class ShikiDecorations(NoExtrasAllowedProps):
- """Decorations for the code block."""
- start: int | Position
- end: int | Position
- tag_name: str = "span"
- properties: dict[str, Any] = {}
- always_wrap: bool = False
- class ShikiBaseTransformers(Base):
- """Base for creating transformers."""
- library: str
- fns: list[FunctionStringVar]
- style: Style | None
- class ShikiJsTransformer(ShikiBaseTransformers):
- """A Wrapped shikijs transformer."""
- library: str = "@shikijs/transformers@3.3.0"
- fns: list[FunctionStringVar] = [
- FunctionStringVar.create(fn) for fn in SHIKIJS_TRANSFORMER_FNS
- ]
- style: Style | None = Style(
- {
- "code": {"line-height": "1.7", "font-size": "0.875em", "display": "grid"},
- # Diffs
- ".diff": {
- "margin": "0 -24px",
- "padding": "0 24px",
- "width": "calc(100% + 48px)",
- "display": "inline-block",
- },
- ".diff.add": {
- "background-color": "rgba(16, 185, 129, .14)",
- "position": "relative",
- },
- ".diff.remove": {
- "background-color": "rgba(244, 63, 94, .14)",
- "opacity": "0.7",
- "position": "relative",
- },
- ".diff.remove:after": {
- "position": "absolute",
- "left": "10px",
- "content": "'-'",
- "color": "#b34e52",
- },
- ".diff.add:after": {
- "position": "absolute",
- "left": "10px",
- "content": "'+'",
- "color": "#18794e",
- },
- # Highlight
- ".highlighted": {
- "background-color": "rgba(142, 150, 170, .14)",
- "margin": "0 -24px",
- "padding": "0 24px",
- "width": "calc(100% + 48px)",
- "display": "inline-block",
- },
- ".highlighted.error": {
- "background-color": "rgba(244, 63, 94, .14)",
- },
- ".highlighted.warning": {
- "background-color": "rgba(234, 179, 8, .14)",
- },
- # Highlighted Word
- ".highlighted-word": {
- "background-color": color("gray", 2),
- "border": f"1px solid {color('gray', 5)}",
- "padding": "1px 3px",
- "margin": "-1px -3px",
- "border-radius": "4px",
- },
- # Focused Lines
- ".has-focused .line:not(.focused)": {
- "opacity": "0.7",
- "filter": "blur(0.095rem)",
- "transition": "filter .35s, opacity .35s",
- },
- ".has-focused:hover .line:not(.focused)": {
- "opacity": "1",
- "filter": "none",
- },
- # White Space
- # ".tab, .space": {
- # "position": "relative", # noqa: ERA001
- # },
- # ".tab::before": {
- # "content": "'⇥'", # noqa: ERA001
- # "position": "absolute", # noqa: ERA001
- # "opacity": "0.3",# noqa: ERA001
- # },
- # ".space::before": {
- # "content": "'·'", # noqa: ERA001
- # "position": "absolute", # noqa: ERA001
- # "opacity": "0.3", # noqa: ERA001
- # },
- }
- )
- def __init__(self, **kwargs):
- """Initialize the transformer.
- Args:
- kwargs: Kwargs to initialize the props.
- """
- fns = kwargs.pop("fns", None)
- style = kwargs.pop("style", None)
- if fns:
- kwargs["fns"] = [
- (
- FunctionStringVar.create(x)
- if not isinstance(x, FunctionStringVar)
- else x
- )
- for x in fns
- ]
- if style:
- kwargs["style"] = Style(style)
- super().__init__(**kwargs)
- class ShikiCodeBlock(Component, MarkdownComponentMap):
- """A Code block."""
- library = "/components/shiki/code"
- tag = "Code"
- alias = "ShikiCode"
- lib_dependencies: list[str] = ["shiki@3.3.0"]
- # The language to use.
- language: Var[LiteralCodeLanguage] = Var.create("python")
- # The theme to use ("light" or "dark").
- theme: Var[LiteralCodeTheme] = Var.create("one-light")
- # The set of themes to use for different modes.
- themes: Var[list[dict[str, Any]] | dict[str, str]]
- # The code to display.
- code: Var[str]
- # The transformers to use for the syntax highlighter.
- transformers: Var[list[ShikiBaseTransformers | dict[str, Any]]] = Var.create([])
- # The decorations to use for the syntax highlighter.
- decorations: Var[list[ShikiDecorations]] = Var.create([])
- @classmethod
- def create(
- cls,
- *children,
- **props,
- ) -> Component:
- """Create a code block component using [shiki syntax highlighter](https://shiki.matsu.io/).
- Args:
- *children: The children of the component.
- **props: The props to pass to the component.
- Returns:
- The code block component.
- """
- # Separate props for the code block and the wrapper
- code_block_props = {}
- code_wrapper_props = {}
- decorations = props.pop("decorations", [])
- class_props = cls.get_props()
- # Distribute props between the code block and wrapper
- for key, value in props.items():
- (code_block_props if key in class_props else code_wrapper_props)[key] = (
- value
- )
- # cast decorations into ShikiDecorations.
- decorations = [
- ShikiDecorations(**decoration)
- if not isinstance(decoration, ShikiDecorations)
- else decoration
- for decoration in decorations
- ]
- code_block_props["decorations"] = decorations
- code_block_props["code"] = children[0]
- code_block = super().create(**code_block_props)
- transformer_styles = {}
- # Collect styles from transformers and wrapper
- for transformer in code_block.transformers._var_value: # pyright: ignore [reportAttributeAccessIssue]
- if isinstance(transformer, ShikiBaseTransformers) and transformer.style:
- transformer_styles.update(transformer.style)
- transformer_styles.update(code_wrapper_props.pop("style", {}))
- return Box.create(
- code_block,
- *children[1:],
- style=Style({**transformer_styles, **BOX_PARENT_STYLING}),
- **code_wrapper_props,
- )
- def add_imports(self) -> dict[str, list[str]]:
- """Add the necessary imports.
- We add all referenced transformer functions as imports from their corresponding
- libraries.
- Returns:
- Imports for the component.
- Raises:
- ValueError: If the transformers are not of type LiteralVar.
- """
- imports = defaultdict(list)
- if not isinstance(self.transformers, LiteralVar):
- raise ValueError(
- f"transformers should be a LiteralVar type. Got {type(self.transformers)} instead."
- )
- for transformer in self.transformers._var_value:
- if isinstance(transformer, ShikiBaseTransformers):
- imports[transformer.library].extend(
- [ImportVar(tag=str(fn)) for fn in transformer.fns]
- )
- if transformer.library not in self.lib_dependencies:
- self.lib_dependencies.append(transformer.library)
- return imports
- @classmethod
- def create_transformer(cls, library: str, fns: list[str]) -> ShikiBaseTransformers:
- """Create a transformer from a third party library.
- Args:
- library: The name of the library.
- fns: The str names of the functions/callables to invoke from the library.
- Returns:
- A transformer for the specified library.
- Raises:
- ValueError: If a supplied function name is not valid str.
- """
- if any(not isinstance(fn_name, str) for fn_name in fns):
- raise ValueError(
- f"the function names should be str names of functions in the specified transformer: {library!r}"
- )
- return ShikiBaseTransformers(
- library=library,
- fns=[FunctionStringVar.create(fn) for fn in fns], # pyright: ignore [reportCallIssue]
- )
- def _render(self, props: dict[str, Any] | None = None):
- """Renders the component with the given properties, processing transformers if present.
- Args:
- props: Optional properties to pass to the render function.
- Returns:
- Rendered component output.
- """
- # Ensure props is initialized from class attributes if not provided
- props = props or {
- attr.rstrip("_"): getattr(self, attr) for attr in self.get_props()
- }
- # Extract transformers and apply transformations
- transformers = props.get("transformers")
- if transformers is not None:
- transformed_values = self._process_transformers(transformers._var_value)
- props["transformers"] = LiteralVar.create(transformed_values)
- return super()._render(props)
- def _process_transformers(self, transformer_list: list) -> list:
- """Processes a list of transformers, applying transformations where necessary.
- Args:
- transformer_list: List of transformer objects or values.
- Returns:
- list: A list of transformed values.
- """
- processed = []
- for transformer in transformer_list:
- if isinstance(transformer, ShikiBaseTransformers):
- processed.extend(fn.call() for fn in transformer.fns)
- else:
- processed.append(transformer)
- return processed
- class ShikiHighLevelCodeBlock(ShikiCodeBlock):
- """High level component for the shiki syntax highlighter."""
- # If this is enabled, the default transformers(shikijs transformer) will be used.
- use_transformers: Var[bool]
- # If this is enabled line numbers will be shown next to the code block.
- show_line_numbers: Var[bool]
- # Whether a copy button should appear.
- can_copy: bool = False
- # copy_button: A custom copy button to override the default one.
- copy_button: Component | bool | None = None
- @classmethod
- def create(
- cls,
- *children,
- **props,
- ) -> Component:
- """Create a code block component using [shiki syntax highlighter](https://shiki.matsu.io/).
- Args:
- *children: The children of the component.
- **props: The props to pass to the component.
- Returns:
- The code block component.
- """
- use_transformers = props.pop("use_transformers", False)
- show_line_numbers = props.pop("show_line_numbers", False)
- language = props.pop("language", None)
- can_copy = props.pop("can_copy", False)
- copy_button = props.pop("copy_button", None)
- if use_transformers:
- props["transformers"] = [ShikiJsTransformer()]
- if language is not None:
- props["language"] = cls._map_languages(language)
- # line numbers are generated via css
- if show_line_numbers:
- props["style"] = {**LINE_NUMBER_STYLING, **props.get("style", {})}
- theme = props.pop("theme", None)
- props["theme"] = props["theme"] = (
- cls._map_themes(theme)
- if theme is not None
- else color_mode_cond( # Default color scheme responds to global color mode.
- light="one-light",
- dark="one-dark-pro",
- )
- )
- if can_copy:
- code = children[0]
- copy_button = (
- copy_button
- if copy_button is not None
- else Button.create(
- Icon.create(tag="copy", size=16, color=color("gray", 11)),
- on_click=[
- set_clipboard(cls._strip_transformer_triggers(code)),
- copy_script(),
- ],
- style=Style(
- {
- "position": "absolute",
- "top": "4px",
- "right": "4px",
- "background": color("gray", 3),
- "border": "1px solid",
- "border-color": color("gray", 5),
- "border-radius": "6px",
- "padding": "5px",
- "opacity": "1",
- "cursor": "pointer",
- "_hover": {
- "background": color("gray", 4),
- },
- "transition": "background 0.250s ease-out",
- "&>svg": {
- "transition": "transform 0.250s ease-out, opacity 0.250s ease-out",
- },
- "_active": {
- "background": color("gray", 5),
- },
- }
- ),
- )
- )
- if copy_button:
- return ShikiCodeBlock.create(
- children[0], copy_button, position="relative", **props
- )
- else:
- return ShikiCodeBlock.create(children[0], **props)
- @staticmethod
- def _map_themes(theme: str) -> str:
- if isinstance(theme, str) and theme in THEME_MAPPING:
- return THEME_MAPPING[theme]
- return theme
- @staticmethod
- def _map_languages(language: str) -> str:
- if isinstance(language, str) and language in LANGUAGE_MAPPING:
- return LANGUAGE_MAPPING[language]
- return language
- @staticmethod
- def _strip_transformer_triggers(code: str | StringVar) -> StringVar | str:
- if not isinstance(code, (StringVar, str)):
- raise VarTypeError(
- f"code should be string literal or a StringVar type. Got {type(code)} instead."
- )
- regex_pattern = r"[\/#]+ *\[!code.*?\]"
- if isinstance(code, Var):
- return string_replace_operation(
- code, StringVar(_js_expr=f"/{regex_pattern}/g", _var_type=str), ""
- )
- if isinstance(code, str):
- return re.sub(regex_pattern, "", code)
- class TransformerNamespace(ComponentNamespace):
- """Namespace for the Transformers."""
- shikijs = ShikiJsTransformer
- class CodeblockNamespace(ComponentNamespace):
- """Namespace for the CodeBlock component."""
- root = staticmethod(ShikiCodeBlock.create)
- create_transformer = staticmethod(ShikiCodeBlock.create_transformer)
- transformers = TransformerNamespace()
- __call__ = staticmethod(ShikiHighLevelCodeBlock.create)
- code_block = CodeblockNamespace()
|