'use client' // @ts-ignore import curl from 'highlightjs-curl' import { noop } from 'lodash' import { Check, Copy } from 'lucide-react' import { useTheme } from 'next-themes' import { Children, memo, ReactNode, useState } from 'react' import { Light as SyntaxHighlighter, SyntaxHighlighterProps } from 'react-syntax-highlighter' import bash from 'react-syntax-highlighter/dist/cjs/languages/hljs/bash' import csharp from 'react-syntax-highlighter/dist/cjs/languages/hljs/csharp' import dart from 'react-syntax-highlighter/dist/cjs/languages/hljs/dart' import go from 'react-syntax-highlighter/dist/cjs/languages/hljs/go' import http from 'react-syntax-highlighter/dist/cjs/languages/hljs/http' import ini from 'react-syntax-highlighter/dist/cjs/languages/hljs/ini' import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript' import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json' import kotlin from 'react-syntax-highlighter/dist/cjs/languages/hljs/kotlin' import markdown from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown' import pgsql from 'react-syntax-highlighter/dist/cjs/languages/hljs/pgsql' import php from 'react-syntax-highlighter/dist/cjs/languages/hljs/php' import { default as py, default as python, } from 'react-syntax-highlighter/dist/cjs/languages/hljs/python' import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql' import swift from 'react-syntax-highlighter/dist/cjs/languages/hljs/swift' import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript' import xml from 'react-syntax-highlighter/dist/cjs/languages/hljs/xml' import yaml from 'react-syntax-highlighter/dist/cjs/languages/hljs/yaml' import { Button, cn, copyToClipboard, FloatingPlate } from 'ui' import { monokaiCustomTheme } from './CodeBlock.utils' SyntaxHighlighter.registerLanguage('js', js) SyntaxHighlighter.registerLanguage('ts', ts) SyntaxHighlighter.registerLanguage('py', py) SyntaxHighlighter.registerLanguage('sql', sql) SyntaxHighlighter.registerLanguage('bash', bash) SyntaxHighlighter.registerLanguage('dart', dart) SyntaxHighlighter.registerLanguage('csharp', csharp) SyntaxHighlighter.registerLanguage('json', json) SyntaxHighlighter.registerLanguage('kotlin', kotlin) SyntaxHighlighter.registerLanguage('curl', curl) SyntaxHighlighter.registerLanguage('http', http) SyntaxHighlighter.registerLanguage('php', php) SyntaxHighlighter.registerLanguage('python', python) SyntaxHighlighter.registerLanguage('go', go) SyntaxHighlighter.registerLanguage('pgsql', pgsql) SyntaxHighlighter.registerLanguage('swift', swift) SyntaxHighlighter.registerLanguage('html', xml) SyntaxHighlighter.registerLanguage('toml', ini) SyntaxHighlighter.registerLanguage('yaml', yaml) SyntaxHighlighter.registerLanguage('markdown', markdown) const codeBlockLangs = [ 'js', 'jsx', 'sql', 'py', 'bash', 'ts', 'dart', 'json', 'csharp', 'kotlin', 'curl', 'http', 'php', 'python', 'go', 'pgsql', 'swift', 'yaml', 'toml', 'html', 'markdown', ] as const export type CodeBlockLang = (typeof codeBlockLangs)[number] export function isCodeBlockLang(lang: string): lang is CodeBlockLang { return (codeBlockLangs as readonly string[]).includes(lang) } export interface CodeBlockProps { title?: ReactNode language?: CodeBlockLang linesToHighlight?: number[] highlightBorder?: boolean styleConfig?: { lineNumber?: string highlightBackgroundColor?: string highlightBorderColor?: string } hideCopy?: boolean hideLineNumbers?: boolean className?: string wrapperClassName?: string value?: string theme?: any children?: string wrapLines?: boolean wrapLongLines?: boolean focusable?: boolean renderer?: SyntaxHighlighterProps['renderer'] handleCopy?: (value?: string) => void onCopyCallback?: () => void } /** * CodeBlock component for displaying syntax-highlighted code. * @param {ReactNode} [props.title] - Optional title for the code block. * @param {string} [props.language] - The programming language of the code. * @param {number[]} [props.linesToHighlight=[]] - Array of line numbers to highlight. * @param {boolean} [props.highlightBorder] - Whether to show a border on highlighted lines. * @param {Object} [props.styleConfig] - Custom style configurations. * @param {string} [props.className] - Additional CSS classes for the code block. * @param {string} [props.wrapperClassName] - CSS classes for the wrapper div. * @param {string} [props.value] - The code content as a string. * @param {any} [props.theme] - Custom theme for syntax highlighting. * @param {string} [props.children] - The code content as children. * @param {boolean} [props.hideCopy=false] - Whether to hide the copy button. * @param {boolean} [props.hideLineNumbers=false] - Whether to hide line numbers. * @param {boolean} [props.wrapLongLines=false] - Whether long lines soft-wrap instead of scrolling horizontally. * @param {SyntaxHighlighterProps['renderer']} [props.renderer] - Custom renderer for syntax highlighting. * @param {boolean} [props.focusable=true] - Whether the code block is focusable. When true, users can focus the code block to select text or use ⌘A (Cmd+A) to select all. This is so we don't need to load Monaco Editor. * @param {function} [props.handleCopy] - Optional override behaviour for copying value. For e.g if the code block contains obfuscated values, but the copy behaviour should reveal those values instead. */ export const CodeBlock = memo(function CodeBlock({ title, language, linesToHighlight = [], highlightBorder, styleConfig, className, wrapperClassName, value, theme, children, hideCopy = false, hideLineNumbers = false, wrapLines = true, wrapLongLines = false, renderer, focusable = true, onCopyCallback = noop, handleCopy, }: CodeBlockProps) { const { resolvedTheme } = useTheme() const isDarkTheme = resolvedTheme?.includes('dark')! const monokaiTheme = theme ?? monokaiCustomTheme(isDarkTheme) const [copied, setCopied] = useState(false) const onSelectCopy = (value?: string) => { if (value) { if (!!handleCopy) { handleCopy(value) } else { copyToClipboard(value) } } setCopied(true) onCopyCallback() setTimeout(() => setCopied(false), 1000) } // Extract string when `children` has a single string node const childrenArray = Children.toArray(children) const [singleChild] = childrenArray.length === 1 ? childrenArray : [] const singleString = typeof singleChild === 'string' ? singleChild : undefined let codeValue = value ?? singleString ?? children codeValue = codeValue?.trimEnd?.() ?? codeValue // check the length of the string inside the tag // if it's fewer than 70 characters, add a white-space: pre so it doesn't wrap const shortCodeBlockClasses = typeof codeValue === 'string' && codeValue.length < 70 ? 'short-inline-codeblock' : '' let lang = language ? language : className ? className.replace('language-', '') : 'js' // force jsx to be js highlighted if (lang === 'jsx') lang = 'js' const large = false // don't show line numbers if bash == lang if (lang === 'bash' || lang === 'sh') hideLineNumbers = true const showLineNumbers = !hideLineNumbers return ( <> {title && (
{title}
)} {className ? (
{/* @ts-ignore */} { if (linesToHighlight.includes(lineNumber)) { return { style: { display: 'block', backgroundColor: styleConfig?.highlightBackgroundColor ? styleConfig?.highlightBackgroundColor : 'var(--background-selection)', borderLeft: highlightBorder ? `1px solid ${styleConfig?.highlightBorderColor ? styleConfig?.highlightBorderColor : 'var(--foreground-default)'}` : undefined, }, class: 'hljs-line-highlight', } } return {} }} lineNumberContainerStyle={{ paddingTop: '128px', }} lineNumberStyle={{ minWidth: '44px', paddingLeft: '4px', paddingRight: '4px', marginRight: '12px', color: styleConfig?.lineNumber ?? '#828282', textAlign: 'center', fontSize: large ? 14 : 12, paddingTop: '4px', paddingBottom: '4px', }} renderer={renderer} contentEditable={focusable} onBeforeInput={(e: any) => { e.preventDefault() return false }} onKeyDown={(e: any) => { if (e.code === 'Backspace') { e.preventDefault() return false } }} > {codeValue} {!hideCopy && (value || children) && className ? ( ) : null}
) : ( {value || children} )} ) })