Created
May 5, 2025 09:32
-
-
Save ElianCodes/9e5c7fddb2c807b924e39fa02898652a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { types, useVisualEdit } from 'react-bricks/astro' | |
| import Editor from 'react-simple-code-editor' | |
| import { useEffect, useState } from 'react' | |
| import { codeToHtml, bundledLanguagesInfo as bundledLanguages, type BundledLanguage } from 'shiki' | |
| interface CodeBlockProps { | |
| lang: BundledLanguage | |
| code: string | |
| lines?: string | |
| addLineNumbers: boolean | |
| themeLight: string | |
| themeDark: string | |
| } | |
| const supportedLanguages = bundledLanguages.map((lang) => ({ | |
| value: lang.id, | |
| label: lang.name | |
| })) | |
| const parseHighlightLines = (input: string | undefined): number[] => { | |
| if (!input) return [] | |
| const lines = input.split(',').flatMap((entry) => { | |
| if (entry.includes('-')) { | |
| const [start, end] = entry.split('-').map(Number) | |
| return Array.from({ length: end - start + 1 }, (_, i) => start + i) | |
| } | |
| return [Number(entry)] | |
| }) | |
| return lines | |
| } | |
| function extractTextFromHTML(html: string) { | |
| const tempDiv = document.createElement('div') | |
| tempDiv.innerHTML = html | |
| return tempDiv.textContent || tempDiv.innerText || '' | |
| } | |
| const CodeBlock: types.Brick<CodeBlockProps> = ({ | |
| lang, | |
| code, | |
| lines, | |
| addLineNumbers, | |
| themeLight, | |
| themeDark, | |
| }: CodeBlockProps) => { | |
| const highlightedLines = parseHighlightLines(lines) | |
| const [changeValue, readonly] = useVisualEdit('code') | |
| const [highlightedCode, setHighlightedCode] = useState(code) | |
| const [isFocus, setIsFocused] = useState(false) | |
| useEffect(() => { | |
| const loadShiki = async () => { | |
| const config = getShikiConfig(code, lang, { light: themeLight, dark: themeDark }, highlightedLines, addLineNumbers) | |
| const html = await codeToHtml(code, config) | |
| setHighlightedCode(html) | |
| } | |
| if (!isFocus) { | |
| loadShiki() | |
| } else { | |
| setHighlightedCode(extractTextFromHTML(code)) | |
| } | |
| }, [readonly, code, lang, lines, isFocus, addLineNumbers, themeLight, themeDark]) | |
| return readonly ? ( | |
| <ReadOnlyCodeBlock highlightedCode={highlightedCode} /> | |
| ) : ( | |
| <EditableCodeBlock | |
| code={code} | |
| highlightedCode={highlightedCode} | |
| onFocus={() => setIsFocused(true)} | |
| onBlur={() => setIsFocused(false)} | |
| onValueChange={(val) => changeValue(val)} | |
| /> | |
| ) | |
| } | |
| CodeBlock.schema = { | |
| name: 'code-block', | |
| label: 'Code Block', | |
| astroInteractivity: 'load', | |
| getDefaultProps: () => ({ | |
| lang: 'tsx', | |
| code: `const a = 'hello'`, | |
| lines: '1', | |
| themeDark: 'nord', | |
| themeLight: 'one-light', | |
| }), | |
| sideEditProps: [ | |
| { | |
| name: 'lang', | |
| label: 'Language', | |
| type: types.SideEditPropType.Select, | |
| selectOptions: { | |
| display: types.OptionsDisplay.Select, | |
| options: supportedLanguages, | |
| }, | |
| }, | |
| { | |
| name: 'lines', | |
| label: 'Lines to highlight', | |
| type: types.SideEditPropType.Text, | |
| }, | |
| { | |
| name: 'addLineNumbers', | |
| label: 'Add line numbers', | |
| type: types.SideEditPropType.Boolean, | |
| }, | |
| { | |
| name: 'themeLight', | |
| label: 'Light theme', | |
| type: types.SideEditPropType.Select, | |
| selectOptions: { | |
| display: types.OptionsDisplay.Select, | |
| options: [ | |
| { value: 'github-light-default', label: 'Github light default' }, | |
| { value: 'github-light', label: 'Github light' }, | |
| { value: 'one-light', label: 'One light' }, | |
| ], | |
| }, | |
| }, | |
| { | |
| name: 'themeDark', | |
| label: 'Dark theme', | |
| type: types.SideEditPropType.Select, | |
| selectOptions: { | |
| display: types.OptionsDisplay.Select, | |
| options: [ | |
| { value: 'github-dark-default', label: 'Github dark default' }, | |
| { value: 'github-dark', label: 'Github dark' }, | |
| { value: 'nord', label: 'Nord' }, | |
| ], | |
| }, | |
| }, | |
| ], | |
| } | |
| export default CodeBlock | |
| const getShikiConfig = (code: string, lang: BundledLanguage, themes: { light: string; dark: string }, highlightedLines: number[], addLineNumbers: boolean) => ({ | |
| lang, | |
| themes, | |
| transformers: [ | |
| { | |
| pre(node) { | |
| node.properties.style = | |
| (node.properties.style || '') + | |
| '; padding: 20px; border-radius:6px; overflow:visible;' | |
| }, | |
| line(node, lineNumber: number) { | |
| if (highlightedLines.includes(lineNumber)) { | |
| node.properties.style = | |
| (node.properties.style || '') + | |
| 'background-color:#fff085; --shiki-dark-bg:#894b00;' | |
| } | |
| }, | |
| }, | |
| addLineNumbers | |
| ? { | |
| pre(node) { | |
| node.properties.class = | |
| (node.properties.class || '') + ' shiki-code' | |
| }, | |
| line(node, lineNumber: number) { | |
| const style = node.properties.style || '' | |
| node.children.unshift({ | |
| type: 'element', | |
| tagName: 'span', | |
| properties: { | |
| className: ['line-number'], | |
| style: | |
| 'display:inline-block; width:2em; user-select:none; opacity:0.5;', | |
| }, | |
| children: [ | |
| { | |
| type: 'text', | |
| value: lineNumber.toString().padStart(2) + ' ', | |
| }, | |
| ], | |
| }) | |
| node.properties.style = style | |
| }, | |
| } | |
| : {}, | |
| ], | |
| }) | |
| const ReadOnlyCodeBlock = ({ highlightedCode }: { highlightedCode: string }) => ( | |
| <div | |
| className="max-w-[692px] mx-auto bg-[#fafafa] dark:bg-[#2e3440] rounded-lg mt-8 mb-6 text-sm" | |
| dangerouslySetInnerHTML={{ __html: highlightedCode }} | |
| /> | |
| ) | |
| const EditableCodeBlock = ({ | |
| code, | |
| highlightedCode, | |
| onFocus, | |
| onBlur, | |
| onValueChange, | |
| }: { | |
| code: string | |
| highlightedCode: string | |
| onFocus: () => void | |
| onBlur: () => void | |
| onValueChange: (val: string) => void | |
| }) => ( | |
| <Editor | |
| value={code} | |
| onFocus={onFocus} | |
| onBlur={onBlur} | |
| onValueChange={onValueChange} | |
| highlight={() => highlightedCode} | |
| padding={10} | |
| style={{ | |
| fontFamily: '"Fira Code", "Fira Mono", monospace', | |
| fontSize: 12, | |
| }} | |
| preClassName="overflow-auto" | |
| className="max-w-[692px] mx-auto rounded-lg mt-8 mb-6 text-sm overflow-auto dark:text-white" | |
| /> | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment