[ENG-3848][ENG-3861]Shiki Code block Experimental (#4030)
* Shiki Code block Experimental * refactor * update code * remove console.log * add transformers to namespace * some validations * fix components paths * fix ruff * add a high-level component * fix mapping * fix mapping * python 3.9+ * see if this fixes the tests * fix pyi and annotations * minimal update of theme and language map * add hack for reflex-web/flexdown * unit test file commit * [ENG-3895] [ENG-3896] Update styling for shiki code block * strip transformer triggers * minor refactor * linter * fix pyright * pyi fix * add unit tests * sneaky pyright ignore * the transformer trigger regex should remove the language comment character * minor refactor * fix silly mistake * component mapping in markdown should use the first child for codeblock * use ternary operator in numbers.py, move code block args to class for docs discoverability * precommit * pyright fix * remove id on copy button animation * pyright fix for real * pyi fix * pyi fix fr * check if svg exists * copy event chain * do a concatenation instead of first child * added comment --------- Co-authored-by: Carlos <cutillascarlos@gmail.com>
This commit is contained in:
parent
c103ab5e28
commit
d63b3a2bce
29
reflex/.templates/web/components/shiki/code.js
Normal file
29
reflex/.templates/web/components/shiki/code.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { codeToHtml} from "shiki"
|
||||||
|
|
||||||
|
export function Code ({code, theme, language, transformers, ...divProps}) {
|
||||||
|
const [codeResult, setCodeResult] = useState("")
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchCode() {
|
||||||
|
let final_code;
|
||||||
|
|
||||||
|
if (Array.isArray(code)) {
|
||||||
|
final_code = code[0];
|
||||||
|
} else {
|
||||||
|
final_code = code;
|
||||||
|
}
|
||||||
|
const result = await codeToHtml(final_code, {
|
||||||
|
lang: language,
|
||||||
|
theme,
|
||||||
|
transformers
|
||||||
|
});
|
||||||
|
setCodeResult(result);
|
||||||
|
}
|
||||||
|
fetchCode();
|
||||||
|
}, [code, language, theme, transformers]
|
||||||
|
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div dangerouslySetInnerHTML={{__html: codeResult}} {...divProps} ></div>
|
||||||
|
)
|
||||||
|
}
|
813
reflex/components/datadisplay/shiki_code_block.py
Normal file
813
reflex/components/datadisplay/shiki_code_block.py
Normal file
@ -0,0 +1,813 @@
|
|||||||
|
"""Shiki syntax hghlighter component."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Literal, Optional, Union
|
||||||
|
|
||||||
|
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.radix.themes.layout.box import Box
|
||||||
|
from reflex.event import call_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 call_script(
|
||||||
|
f"""
|
||||||
|
// Event listener for the parent click
|
||||||
|
document.addEventListener('click', function(event) {{
|
||||||
|
// Find the closest div (parent element)
|
||||||
|
const parent = event.target.closest('div');
|
||||||
|
// 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",
|
||||||
|
"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",
|
||||||
|
"plain",
|
||||||
|
"plastic",
|
||||||
|
"poimandres",
|
||||||
|
"red",
|
||||||
|
"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 ShikiBaseTransformers(Base):
|
||||||
|
"""Base for creating transformers."""
|
||||||
|
|
||||||
|
library: str
|
||||||
|
fns: list[FunctionStringVar]
|
||||||
|
style: Optional[Style]
|
||||||
|
|
||||||
|
|
||||||
|
class ShikiJsTransformer(ShikiBaseTransformers):
|
||||||
|
"""A Wrapped shikijs transformer."""
|
||||||
|
|
||||||
|
library: str = "@shikijs/transformers"
|
||||||
|
fns: list[FunctionStringVar] = [
|
||||||
|
FunctionStringVar.create(fn) for fn in SHIKIJS_TRANSFORMER_FNS
|
||||||
|
]
|
||||||
|
style: Optional[Style] = 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",
|
||||||
|
# },
|
||||||
|
# ".tab::before": {
|
||||||
|
# "content": "'⇥'",
|
||||||
|
# "position": "absolute",
|
||||||
|
# "opacity": "0.3",
|
||||||
|
# },
|
||||||
|
# ".space::before": {
|
||||||
|
# "content": "'·'",
|
||||||
|
# "position": "absolute",
|
||||||
|
# "opacity": "0.3",
|
||||||
|
# },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""A Code block."""
|
||||||
|
|
||||||
|
library = "/components/shiki/code"
|
||||||
|
|
||||||
|
tag = "Code"
|
||||||
|
|
||||||
|
alias = "ShikiCode"
|
||||||
|
|
||||||
|
lib_dependencies: list[str] = ["shiki"]
|
||||||
|
|
||||||
|
# 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[Union[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[Union[ShikiBaseTransformers, dict[str, Any]]]] = 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 = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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: # type: ignore
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
imports = defaultdict(list)
|
||||||
|
for transformer in self.transformers._var_value:
|
||||||
|
if isinstance(transformer, ShikiBaseTransformers):
|
||||||
|
imports[transformer.library].extend(
|
||||||
|
[ImportVar(tag=str(fn)) for fn in transformer.fns]
|
||||||
|
)
|
||||||
|
(
|
||||||
|
self.lib_dependencies.append(transformer.library)
|
||||||
|
if transformer.library not in self.lib_dependencies
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
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( # type: ignore
|
||||||
|
library=library, fns=[FunctionStringVar.create(fn) for fn in fns]
|
||||||
|
)
|
||||||
|
|
||||||
|
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: Var[bool] = Var.create(False)
|
||||||
|
|
||||||
|
# copy_button: A custom copy button to override the default one.
|
||||||
|
copy_button: Var[Optional[Union[Component, bool]]] = Var.create(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 = ( # type: ignore
|
||||||
|
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)), # type: ignore
|
||||||
|
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()
|
2211
reflex/components/datadisplay/shiki_code_block.pyi
Normal file
2211
reflex/components/datadisplay/shiki_code_block.pyi
Normal file
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,8 @@ from reflex.components.tags.tag import Tag
|
|||||||
from reflex.utils import types
|
from reflex.utils import types
|
||||||
from reflex.utils.imports import ImportDict, ImportVar
|
from reflex.utils.imports import ImportDict, ImportVar
|
||||||
from reflex.vars.base import LiteralVar, Var
|
from reflex.vars.base import LiteralVar, Var
|
||||||
|
from reflex.vars.function import ARRAY_ISARRAY
|
||||||
|
from reflex.vars.number import ternary_operation
|
||||||
|
|
||||||
# Special vars used in the component map.
|
# Special vars used in the component map.
|
||||||
_CHILDREN = Var(_js_expr="children", _var_type=str)
|
_CHILDREN = Var(_js_expr="children", _var_type=str)
|
||||||
@ -199,7 +201,16 @@ class Markdown(Component):
|
|||||||
raise ValueError(f"No markdown component found for tag: {tag}.")
|
raise ValueError(f"No markdown component found for tag: {tag}.")
|
||||||
|
|
||||||
special_props = [_PROPS_IN_TAG]
|
special_props = [_PROPS_IN_TAG]
|
||||||
children = [_CHILDREN]
|
children = [
|
||||||
|
_CHILDREN
|
||||||
|
if tag != "codeblock"
|
||||||
|
# For codeblock, the mapping for some cases returns an array of elements. Let's join them into a string.
|
||||||
|
else ternary_operation(
|
||||||
|
ARRAY_ISARRAY.call(_CHILDREN), # type: ignore
|
||||||
|
_CHILDREN.to(list).join("\n"),
|
||||||
|
_CHILDREN,
|
||||||
|
).to(str)
|
||||||
|
]
|
||||||
|
|
||||||
# For certain tags, the props from the markdown renderer are not actually valid for the component.
|
# For certain tags, the props from the markdown renderer are not actually valid for the component.
|
||||||
if tag in NO_PROPS_TAGS:
|
if tag in NO_PROPS_TAGS:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from reflex.components.datadisplay.shiki_code_block import code_block as code_block
|
||||||
from reflex.components.props import PropsBase
|
from reflex.components.props import PropsBase
|
||||||
from reflex.components.radix.themes.components.progress import progress as progress
|
from reflex.components.radix.themes.components.progress import progress as progress
|
||||||
from reflex.components.sonner.toast import toast as toast
|
from reflex.components.sonner.toast import toast as toast
|
||||||
@ -67,4 +68,5 @@ _x = ExperimentalNamespace(
|
|||||||
layout=layout,
|
layout=layout,
|
||||||
PropsBase=PropsBase,
|
PropsBase=PropsBase,
|
||||||
run_in_thread=run_in_thread,
|
run_in_thread=run_in_thread,
|
||||||
|
code_block=code_block,
|
||||||
)
|
)
|
||||||
|
@ -180,6 +180,7 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar):
|
|||||||
|
|
||||||
|
|
||||||
JSON_STRINGIFY = FunctionStringVar.create("JSON.stringify")
|
JSON_STRINGIFY = FunctionStringVar.create("JSON.stringify")
|
||||||
|
ARRAY_ISARRAY = FunctionStringVar.create("Array.isArray")
|
||||||
PROTOTYPE_TO_STRING = FunctionStringVar.create(
|
PROTOTYPE_TO_STRING = FunctionStringVar.create(
|
||||||
"((__to_string) => __to_string.toString())"
|
"((__to_string) => __to_string.toString())"
|
||||||
)
|
)
|
||||||
|
@ -529,6 +529,26 @@ def array_join_operation(array: ArrayVar, sep: StringVar[Any] | str = ""):
|
|||||||
return var_operation_return(js_expression=f"{array}.join({sep})", var_type=str)
|
return var_operation_return(js_expression=f"{array}.join({sep})", var_type=str)
|
||||||
|
|
||||||
|
|
||||||
|
@var_operation
|
||||||
|
def string_replace_operation(
|
||||||
|
string: StringVar, search_value: StringVar | str, new_value: StringVar | str
|
||||||
|
):
|
||||||
|
"""Replace a string with a value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
string: The string.
|
||||||
|
search_value: The string to search.
|
||||||
|
new_value: The value to be replaced with.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The string replace operation.
|
||||||
|
"""
|
||||||
|
return var_operation_return(
|
||||||
|
js_expression=f"{string}.replace({search_value}, {new_value})",
|
||||||
|
var_type=str,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Compile regex for finding reflex var tags.
|
# Compile regex for finding reflex var tags.
|
||||||
_decode_var_pattern_re = (
|
_decode_var_pattern_re = (
|
||||||
rf"{constants.REFLEX_VAR_OPENING_TAG}(.*?){constants.REFLEX_VAR_CLOSING_TAG}"
|
rf"{constants.REFLEX_VAR_OPENING_TAG}(.*?){constants.REFLEX_VAR_CLOSING_TAG}"
|
||||||
|
172
tests/units/components/datadisplay/test_shiki_code.py
Normal file
172
tests/units/components/datadisplay/test_shiki_code.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from reflex.components.datadisplay.shiki_code_block import (
|
||||||
|
ShikiBaseTransformers,
|
||||||
|
ShikiCodeBlock,
|
||||||
|
ShikiHighLevelCodeBlock,
|
||||||
|
ShikiJsTransformer,
|
||||||
|
)
|
||||||
|
from reflex.components.el.elements.forms import Button
|
||||||
|
from reflex.components.lucide.icon import Icon
|
||||||
|
from reflex.components.radix.themes.layout.box import Box
|
||||||
|
from reflex.style import Style
|
||||||
|
from reflex.vars import Var
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"library, fns, expected_output, raises_exception",
|
||||||
|
[
|
||||||
|
("some_library", ["function_one"], ["function_one"], False),
|
||||||
|
("some_library", [123], None, True),
|
||||||
|
("some_library", [], [], False),
|
||||||
|
(
|
||||||
|
"some_library",
|
||||||
|
["function_one", "function_two"],
|
||||||
|
["function_one", "function_two"],
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
("", ["function_one"], ["function_one"], False),
|
||||||
|
("some_library", ["function_one", 789], None, True),
|
||||||
|
("", [], [], False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_create_transformer(library, fns, expected_output, raises_exception):
|
||||||
|
if raises_exception:
|
||||||
|
# Ensure ValueError is raised for invalid cases
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ShikiCodeBlock.create_transformer(library, fns)
|
||||||
|
else:
|
||||||
|
transformer = ShikiCodeBlock.create_transformer(library, fns)
|
||||||
|
assert isinstance(transformer, ShikiBaseTransformers)
|
||||||
|
assert transformer.library == library
|
||||||
|
|
||||||
|
# Verify that the functions are correctly wrapped in FunctionStringVar
|
||||||
|
function_names = [str(fn) for fn in transformer.fns]
|
||||||
|
assert function_names == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"code_block, children, props, expected_first_child, expected_styles",
|
||||||
|
[
|
||||||
|
("print('Hello')", ["print('Hello')"], {}, "print('Hello')", {}),
|
||||||
|
(
|
||||||
|
"print('Hello')",
|
||||||
|
["print('Hello')", "More content"],
|
||||||
|
{},
|
||||||
|
"print('Hello')",
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"print('Hello')",
|
||||||
|
["print('Hello')"],
|
||||||
|
{
|
||||||
|
"transformers": [
|
||||||
|
ShikiBaseTransformers(
|
||||||
|
library="lib", fns=[], style=Style({"color": "red"})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"print('Hello')",
|
||||||
|
{"color": "red"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"print('Hello')",
|
||||||
|
["print('Hello')"],
|
||||||
|
{
|
||||||
|
"transformers": [
|
||||||
|
ShikiBaseTransformers(
|
||||||
|
library="lib", fns=[], style=Style({"color": "red"})
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"style": {"background": "blue"},
|
||||||
|
},
|
||||||
|
"print('Hello')",
|
||||||
|
{"color": "red", "background": "blue"},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_create_shiki_code_block(
|
||||||
|
code_block, children, props, expected_first_child, expected_styles
|
||||||
|
):
|
||||||
|
component = ShikiCodeBlock.create(code_block, *children, **props)
|
||||||
|
|
||||||
|
# Test that the created component is a Box
|
||||||
|
assert isinstance(component, Box)
|
||||||
|
|
||||||
|
# Test that the first child is the code
|
||||||
|
code_block_component = component.children[0]
|
||||||
|
assert code_block_component.code._var_value == expected_first_child # type: ignore
|
||||||
|
|
||||||
|
applied_styles = component.style
|
||||||
|
for key, value in expected_styles.items():
|
||||||
|
assert Var.create(applied_styles[key])._var_value == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"children, props, expected_transformers, expected_button_type",
|
||||||
|
[
|
||||||
|
(["print('Hello')"], {"use_transformers": True}, [ShikiJsTransformer], None),
|
||||||
|
(["print('Hello')"], {"can_copy": True}, None, Button),
|
||||||
|
(
|
||||||
|
["print('Hello')"],
|
||||||
|
{
|
||||||
|
"can_copy": True,
|
||||||
|
"copy_button": Button.create(Icon.create(tag="a_arrow_down")),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
Button,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_create_shiki_high_level_code_block(
|
||||||
|
children, props, expected_transformers, expected_button_type
|
||||||
|
):
|
||||||
|
component = ShikiHighLevelCodeBlock.create(*children, **props)
|
||||||
|
|
||||||
|
# Test that the created component is a Box
|
||||||
|
assert isinstance(component, Box)
|
||||||
|
|
||||||
|
# Test that the first child is the code block component
|
||||||
|
code_block_component = component.children[0]
|
||||||
|
assert code_block_component.code._var_value == children[0] # type: ignore
|
||||||
|
|
||||||
|
# Check if the transformer is set correctly if expected
|
||||||
|
if expected_transformers:
|
||||||
|
exp_trans_names = [t.__name__ for t in expected_transformers]
|
||||||
|
for transformer in code_block_component.transformers._var_value: # type: ignore
|
||||||
|
assert type(transformer).__name__ in exp_trans_names
|
||||||
|
|
||||||
|
# Check if the second child is the copy button if can_copy is True
|
||||||
|
if props.get("can_copy", False):
|
||||||
|
if props.get("copy_button"):
|
||||||
|
assert isinstance(component.children[1], expected_button_type)
|
||||||
|
assert component.children[1] == props["copy_button"]
|
||||||
|
else:
|
||||||
|
assert isinstance(component.children[1], expected_button_type)
|
||||||
|
else:
|
||||||
|
assert len(component.children) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"children, props",
|
||||||
|
[
|
||||||
|
(["print('Hello')"], {"theme": "dark"}),
|
||||||
|
(["print('Hello')"], {"language": "javascript"}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_shiki_high_level_code_block_theme_language_mapping(children, props):
|
||||||
|
component = ShikiHighLevelCodeBlock.create(*children, **props)
|
||||||
|
|
||||||
|
# Test that the theme is mapped correctly
|
||||||
|
if "theme" in props:
|
||||||
|
assert component.children[
|
||||||
|
0
|
||||||
|
].theme._var_value == ShikiHighLevelCodeBlock._map_themes(props["theme"]) # type: ignore
|
||||||
|
|
||||||
|
# Test that the language is mapped correctly
|
||||||
|
if "language" in props:
|
||||||
|
assert component.children[
|
||||||
|
0
|
||||||
|
].language._var_value == ShikiHighLevelCodeBlock._map_languages( # type: ignore
|
||||||
|
props["language"]
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user