CodeBlock.tsx278 lines · main
1'use client'
2
3// @ts-ignore
4import curl from 'highlightjs-curl'
5import { noop } from 'lodash'
6import { Check, Copy } from 'lucide-react'
7import { useTheme } from 'next-themes'
8import { Children, ReactNode, useState } from 'react'
9import { Light as SyntaxHighlighter, SyntaxHighlighterProps } from 'react-syntax-highlighter'
10import bash from 'react-syntax-highlighter/dist/cjs/languages/hljs/bash'
11import csharp from 'react-syntax-highlighter/dist/cjs/languages/hljs/csharp'
12import dart from 'react-syntax-highlighter/dist/cjs/languages/hljs/dart'
13import go from 'react-syntax-highlighter/dist/cjs/languages/hljs/go'
14import http from 'react-syntax-highlighter/dist/cjs/languages/hljs/http'
15import ini from 'react-syntax-highlighter/dist/cjs/languages/hljs/ini'
16import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript'
17import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json'
18import kotlin from 'react-syntax-highlighter/dist/cjs/languages/hljs/kotlin'
19import pgsql from 'react-syntax-highlighter/dist/cjs/languages/hljs/pgsql'
20import php from 'react-syntax-highlighter/dist/cjs/languages/hljs/php'
21import {
22 default as py,
23 default as python,
24} from 'react-syntax-highlighter/dist/cjs/languages/hljs/python'
25import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'
26import swift from 'react-syntax-highlighter/dist/cjs/languages/hljs/swift'
27import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript'
28import xml from 'react-syntax-highlighter/dist/cjs/languages/hljs/xml'
29import yaml from 'react-syntax-highlighter/dist/cjs/languages/hljs/yaml'
30import { Button, cn, copyToClipboard } from 'ui'
31
32import { monokaiCustomTheme } from './CodeBlock.utils'
33
34export type CodeBlockLang =
35 | 'js'
36 | 'jsx'
37 | 'sql'
38 | 'py'
39 | 'bash'
40 | 'ts'
41 | 'dart'
42 | 'json'
43 | 'csharp'
44 | 'kotlin'
45 | 'curl'
46 | 'http'
47 | 'php'
48 | 'python'
49 | 'go'
50 | 'pgsql'
51 | 'swift'
52 | 'yaml'
53 | 'toml'
54 | 'html'
55
56export interface CodeBlockProps {
57 title?: ReactNode
58 language?: CodeBlockLang
59 linesToHighlight?: number[]
60 highlightBorder?: boolean
61 styleConfig?: {
62 lineNumber?: string
63 highlightBackgroundColor?: string
64 highlightBorderColor?: string
65 }
66 hideCopy?: boolean
67 hideLineNumbers?: boolean
68 className?: string
69 wrapperClassName?: string
70 value?: string
71 theme?: any
72 children?: string
73 wrapLines?: boolean
74 focusable?: boolean
75 renderer?: SyntaxHighlighterProps['renderer']
76 handleCopy?: (value?: string) => void
77 onCopyCallback?: () => void
78}
79
80/**
81 * CodeBlock component for displaying syntax-highlighted code.
82 * @param {ReactNode} [props.title] - Optional title for the code block.
83 * @param {string} [props.language] - The programming language of the code.
84 * @param {number[]} [props.linesToHighlight=[]] - Array of line numbers to highlight.
85 * @param {boolean} [props.highlightBorder] - Whether to show a border on highlighted lines.
86 * @param {Object} [props.styleConfig] - Custom style configurations.
87 * @param {string} [props.className] - Additional CSS classes for the code block.
88 * @param {string} [props.wrapperClassName] - CSS classes for the wrapper div.
89 * @param {string} [props.value] - The code content as a string.
90 * @param {any} [props.theme] - Custom theme for syntax highlighting.
91 * @param {string} [props.children] - The code content as children.
92 * @param {boolean} [props.hideCopy=false] - Whether to hide the copy button.
93 * @param {boolean} [props.hideLineNumbers=false] - Whether to hide line numbers.
94 * @param {SyntaxHighlighterProps['renderer']} [props.renderer] - Custom renderer for syntax highlighting.
95 * @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.
96 * @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.
97 */
98export const CodeBlock = ({
99 title,
100 language,
101 linesToHighlight = [],
102 highlightBorder,
103 styleConfig,
104 className,
105 wrapperClassName,
106 value,
107 theme,
108 children,
109 hideCopy = false,
110 hideLineNumbers = false,
111 wrapLines = true,
112 renderer,
113 focusable = true,
114 onCopyCallback = noop,
115 handleCopy,
116}: CodeBlockProps) => {
117 const { resolvedTheme } = useTheme()
118 const isDarkTheme = resolvedTheme?.includes('dark')!
119 const monokaiTheme = theme ?? monokaiCustomTheme(isDarkTheme)
120
121 const [copied, setCopied] = useState(false)
122
123 const onSelectCopy = (value?: string) => {
124 if (value) {
125 if (!!handleCopy) {
126 handleCopy(value)
127 } else {
128 copyToClipboard(value)
129 }
130 }
131 setCopied(true)
132 onCopyCallback()
133 setTimeout(() => setCopied(false), 1000)
134 }
135
136 // Extract string when `children` has a single string node
137 const childrenArray = Children.toArray(children)
138 const [singleChild] = childrenArray.length === 1 ? childrenArray : []
139 const singleString = typeof singleChild === 'string' ? singleChild : undefined
140
141 let codeValue = value ?? singleString ?? children
142 codeValue = codeValue?.trimEnd?.() ?? codeValue
143
144 // check the length of the string inside the <code> tag
145 // if it's fewer than 70 characters, add a white-space: pre so it doesn't wrap
146 const shortCodeBlockClasses =
147 typeof codeValue === 'string' && codeValue.length < 70 ? 'short-inline-codeblock' : ''
148
149 let lang = language ? language : className ? className.replace('language-', '') : 'js'
150 // force jsx to be js highlighted
151 if (lang === 'jsx') lang = 'js'
152 SyntaxHighlighter.registerLanguage('js', js)
153 SyntaxHighlighter.registerLanguage('ts', ts)
154 SyntaxHighlighter.registerLanguage('py', py)
155 SyntaxHighlighter.registerLanguage('sql', sql)
156 SyntaxHighlighter.registerLanguage('bash', bash)
157 SyntaxHighlighter.registerLanguage('dart', dart)
158 SyntaxHighlighter.registerLanguage('csharp', csharp)
159 SyntaxHighlighter.registerLanguage('json', json)
160 SyntaxHighlighter.registerLanguage('kotlin', kotlin)
161 SyntaxHighlighter.registerLanguage('curl', curl)
162 SyntaxHighlighter.registerLanguage('http', http)
163 SyntaxHighlighter.registerLanguage('php', php)
164 SyntaxHighlighter.registerLanguage('python', python)
165 SyntaxHighlighter.registerLanguage('go', go)
166 SyntaxHighlighter.registerLanguage('pgsql', pgsql)
167 SyntaxHighlighter.registerLanguage('swift', swift)
168 SyntaxHighlighter.registerLanguage('html', xml)
169 SyntaxHighlighter.registerLanguage('toml', ini)
170 SyntaxHighlighter.registerLanguage('yaml', yaml)
171
172 const large = false
173 // don't show line numbers if bash == lang
174 if (lang === 'bash' || lang === 'sh') hideLineNumbers = true
175 const showLineNumbers = !hideLineNumbers
176
177 return (
178 <>
179 {title && (
180 <div className="text-sm rounded-t-md bg-surface-100 py-2 px-4 border border-b-0 border-default font-sans">
181 {title}
182 </div>
183 )}
184 {className ? (
185 <div
186 className={cn(
187 'group relative max-w-[90vw] md:max-w-none overflow-auto',
188 wrapperClassName
189 )}
190 >
191 {/* @ts-ignore */}
192 <SyntaxHighlighter
193 suppressContentEditableWarning
194 language={lang}
195 wrapLines={wrapLines}
196 style={monokaiTheme}
197 className={cn(
198 'code-block border border-surface p-4 w-full my-0! !bg-surface-100 outline-hidden focus:border-foreground-lighter/50',
199 `${!title ? 'rounded-md' : 'rounded-t-none rounded-b-md'}`,
200 `${!showLineNumbers ? 'pl-6' : ''}`,
201 className
202 )}
203 customStyle={{
204 fontSize: large ? 18 : 13,
205 lineHeight: large ? 1.5 : 1.4,
206 }}
207 showLineNumbers={showLineNumbers}
208 lineProps={(lineNumber) => {
209 if (linesToHighlight.includes(lineNumber)) {
210 return {
211 style: {
212 display: 'block',
213 backgroundColor: styleConfig?.highlightBackgroundColor
214 ? styleConfig?.highlightBackgroundColor
215 : 'hsl(var(--background-selection))',
216 borderLeft: highlightBorder
217 ? `1px solid ${styleConfig?.highlightBorderColor ? styleConfig?.highlightBorderColor : 'hsl(var(--foreground-default)'})`
218 : null,
219 },
220 class: 'hljs-line-highlight',
221 }
222 }
223 return {}
224 }}
225 lineNumberContainerStyle={{
226 paddingTop: '128px',
227 }}
228 lineNumberStyle={{
229 minWidth: '44px',
230 paddingLeft: '4px',
231 paddingRight: '4px',
232 marginRight: '12px',
233 color: styleConfig?.lineNumber ?? '#828282',
234 textAlign: 'center',
235 fontSize: large ? 14 : 12,
236 paddingTop: '4px',
237 paddingBottom: '4px',
238 }}
239 renderer={renderer}
240 contentEditable={focusable}
241 onBeforeInput={(e: any) => {
242 e.preventDefault()
243 return false
244 }}
245 onKeyDown={(e: any) => {
246 if (e.code === 'Backspace') {
247 e.preventDefault()
248 return false
249 }
250 }}
251 >
252 {codeValue}
253 </SyntaxHighlighter>
254 {!hideCopy && (value || children) && className ? (
255 <div
256 className={[
257 'absolute right-2 top-2',
258 'opacity-0 group-hover:opacity-100 transition',
259 `${isDarkTheme ? 'dark' : ''}`,
260 ].join(' ')}
261 >
262 <Button
263 type="default"
264 className="px-1.5"
265 icon={copied ? <Check /> : <Copy />}
266 onClick={() => onSelectCopy(value || children)}
267 >
268 {copied ? 'Copied' : ''}
269 </Button>
270 </div>
271 ) : null}
272 </div>
273 ) : (
274 <code className={shortCodeBlockClasses}>{value || children}</code>
275 )}
276 </>
277 )
278}