revert codeblock

This commit is contained in:
Saxon Fletcher committed 2026-02-02 11:52:41 +10:00
1 parent e76176cdd4
commit 2d89df2f63
3 files changed
+184 -413

No files matched your search

-4
View File
@@ -582,10 +582,6 @@
"import": "./src/admonition.tsx",
"types": "./src/admonition.tsx"
},
"./multiple-code-block": {
"import": "./src/multiple-code-block.tsx",
"types": "./src/multiple-code-block.tsx"
},
"./consent": {
"import": "./src/consent.tsx",
"types": "./src/consent.tsx"
@@ -1,151 +0,0 @@
import {
CodeBlock,
CodeBlockLang,
TabsContent_Shadcn_,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
Tabs_Shadcn_,
} from 'ui'
interface MultipleCodeBlockFile {
name: string
code: string
language?: string
}
interface MultipleCodeBlockProps {
files: MultipleCodeBlockFile[]
value?: string
onValueChange?: (value: string) => void
}
const languageAliases: Record<string, CodeBlockLang> = {
bash: 'bash',
csharp: 'csharp',
cs: 'csharp',
curl: 'curl',
dart: 'dart',
go: 'go',
html: 'html',
http: 'http',
javascript: 'js',
js: 'js',
json: 'json',
jsx: 'jsx',
kotlin: 'kotlin',
pgsql: 'pgsql',
php: 'php',
py: 'python',
python: 'python',
sh: 'bash',
shell: 'bash',
sql: 'sql',
swift: 'swift',
ts: 'ts',
typescript: 'ts',
yaml: 'yaml',
yml: 'yaml',
}
const extensionLanguageMap: Record<string, CodeBlockLang> = {
astro: 'html',
bash: 'bash',
cjs: 'js',
dart: 'dart',
go: 'go',
htm: 'html',
html: 'html',
js: 'js',
json: 'json',
jsx: 'jsx',
kt: 'kotlin',
mjs: 'js',
php: 'php',
pgsql: 'pgsql',
py: 'python',
sh: 'bash',
sql: 'sql',
swift: 'swift',
svelte: 'html',
ts: 'ts',
vue: 'html',
yaml: 'yaml',
yml: 'yaml',
}
const inferLanguageFromName = (name: string): CodeBlockLang | undefined => {
const lowerName = name.toLowerCase()
if (lowerName.startsWith('.env')) {
return 'bash'
}
const extension = lowerName.split('.').pop()
if (!extension || extension === lowerName) {
return undefined
}
return extensionLanguageMap[extension]
}
const resolveLanguage = (language: string | undefined, name: string): CodeBlockLang => {
if (language) {
const normalized = language.toLowerCase()
const resolved = languageAliases[normalized]
if (resolved) {
return resolved
}
}
return inferLanguageFromName(name) ?? 'js'
}
export const MultipleCodeBlock = ({ files, value, onValueChange }: MultipleCodeBlockProps) => {
if (!files?.length) {
return null
}
const defaultValue = files[0]?.name ?? ''
const trimmedFiles = files.map((file) => ({
...file,
code: typeof file.code === 'string' ? file.code.trim() : file.code,
}))
return (
<Tabs_Shadcn_
defaultValue={defaultValue}
value={value}
onValueChange={onValueChange}
className="border rounded-lg gap-0 space-y-0 overflow-hidden"
>
<TabsList_Shadcn_ className="bg-surface-75 px-5 gap-5 overflow-x-auto border-0 border-b">
{files.map((file) => (
<TabsTrigger_Shadcn_
key={file.name}
value={file.name}
className="flex items-center gap-1 text-xs px-0 data-[state=active]:bg-transparent py-2.5"
>
{file.name}
</TabsTrigger_Shadcn_>
))}
</TabsList_Shadcn_>
{trimmedFiles.map((file) => (
<TabsContent_Shadcn_
key={file.name}
value={file.name}
forceMount
className="p-0 max-h-72 overflow-scroll data-[state=inactive]:hidden"
data-connect-tab-content
data-tab-label={file.name}
>
<CodeBlock
value={typeof file.code === 'string' ? file.code.trim() : file.code}
language={resolveLanguage(file.language, file.name)}
className="min-h-72 !bg-surface-75 rounded-none border-0"
/>
</TabsContent_Shadcn_>
))}
</Tabs_Shadcn_>
)
}
+184 -258
View File
@@ -1,275 +1,201 @@
'use client'
import { Fragment, type PropsWithChildren } from 'react'
import { type BundledLanguage, type ThemedToken, bundledLanguages, createHighlighter } from 'shiki'
import { type ExtraFiles, type NodeHover, createTwoslasher } from 'twoslash'
import { cn } from 'ui'
import curl from 'highlightjs-curl'
import { noop } from 'lodash'
import { Check, Copy } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Children, 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 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 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 yaml from 'react-syntax-highlighter/dist/cjs/languages/hljs/yaml'
import { AnnotatedSpan, CodeBlockControls } from './CodeBlock.client'
import { getFontStyle } from './CodeBlock.utils'
import theme from './supabase-2.json' with { type: 'json' }
import denoTypes from './types/lib.deno.d.ts.include'
import { copyToClipboard } from '../../lib/utils'
import { cn } from '../../lib/utils/cn'
import { Button } from '../Button/Button'
import { monokaiCustomTheme } from './CodeBlock.utils'
const extraFiles: ExtraFiles = { 'deno.d.ts': denoTypes }
export type CodeBlockLang =
| 'js'
| 'jsx'
| 'sql'
| 'py'
| 'bash'
| 'ts'
| 'dart'
| 'json'
| 'csharp'
| 'kotlin'
| 'curl'
| 'http'
| 'php'
| 'python'
| 'go'
| 'pgsql'
| 'swift'
| 'yaml'
| 'toml'
| 'html'
const twoslasher = createTwoslasher({ extraFiles })
const TWOSLASHABLE_LANGS: ReadonlyArray<string> = ['js', 'ts', 'javascript', 'typescript']
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
focusable?: boolean
renderer?: SyntaxHighlighterProps['renderer']
handleCopy?: (value?: string) => void
onCopyCallback?: () => void
}
const BUNDLED_LANGUAGES = Object.keys(bundledLanguages)
const highlighter = await createHighlighter({
themes: [theme],
langs: BUNDLED_LANGUAGES,
})
/**
* 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 {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 = ({
title,
language,
linesToHighlight = [],
highlightBorder,
styleConfig,
export async function CodeBlock({
className,
wrapperClassName,
value,
theme,
lang: langSetting,
lineNumbers = true,
contents,
children,
hideCopy = false,
hideLineNumbers = false,
wrapLines = true,
renderer,
focusable = true,
onCopyCallback = noop,
handleCopy,
}: CodeBlockProps) => {
const { resolvedTheme } = useTheme()
const isDarkTheme = resolvedTheme?.includes('dark')!
const monokaiTheme = theme ?? monokaiCustomTheme(isDarkTheme)
skipTypeGeneration,
}: PropsWithChildren<{
className?: string
lang?: string
lineNumbers?: boolean
contents?: string
skipTypeGeneration?: boolean
}>) {
let code = (contents || extractCode(children)).trim()
const lang = tryToBundledLanguage(langSetting || '') || extractLang(children)
const [copied, setCopied] = useState(false)
const onSelectCopy = (value?: string) => {
if (value) {
if (!!handleCopy) {
handleCopy(value)
} else {
copyToClipboard(value)
}
let twoslashed = null as null | Map<number, Map<number, Array<NodeHover>>>
if (!skipTypeGeneration && lang && TWOSLASHABLE_LANGS.includes(lang)) {
try {
const { code: editedCode, nodes } = twoslasher(code)
const hoverNodes: Array<NodeHover> = nodes.filter((node) => node.type === 'hover')
twoslashed = annotationsByLine(hoverNodes)
code = editedCode
} catch (_err) {
// Silently ignore, if imports aren't defined type compilation fails
// Uncomment lines below to debug in dev
// console.log('\n==========CODE==========\n')
// console.log(code)
// console.error(_err.recommendation)
}
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 <code> 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'
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('yaml', yaml)
const large = false
// don't show line numbers if bash == lang
if (lang === 'bash' || lang === 'sh') hideLineNumbers = true
const showLineNumbers = !hideLineNumbers
const { tokens } = highlighter.codeToTokens(code, {
lang: lang || undefined,
theme: 'Supabase Theme',
})
return (
<>
{title && (
<div className="text-sm rounded-t-md bg-surface-100 py-2 px-4 border border-b-0 border-default font-sans">
{title}
</div>
<div
className={cn(
'shiki',
'group',
'relative',
'not-prose',
'w-full overflow-x-auto',
'border border-default rounded-lg',
'bg-200',
'text-sm',
className
)}
{className ? (
<div
className={cn(
'group relative max-w-[90vw] md:max-w-none overflow-auto',
wrapperClassName
)}
>
{/* @ts-ignore */}
<SyntaxHighlighter
suppressContentEditableWarning
language={lang}
wrapLines={wrapLines}
style={monokaiTheme}
className={cn(
'code-block border border-surface p-4 w-full !my-0 !bg-surface-100 outline-none focus:border-foreground-lighter/50',
`${!title ? 'rounded-md' : 'rounded-t-none rounded-b-md'}`,
`${!showLineNumbers ? 'pl-6' : ''}`,
className
)}
customStyle={{
fontSize: large ? 18 : 13,
lineHeight: large ? 1.5 : 1.4,
}}
showLineNumbers={showLineNumbers}
lineProps={(lineNumber) => {
if (linesToHighlight.includes(lineNumber)) {
return {
style: {
display: 'block',
backgroundColor: styleConfig?.highlightBackgroundColor
? styleConfig?.highlightBackgroundColor
: 'hsl(var(--background-selection))',
borderLeft: highlightBorder
? `1px solid ${styleConfig?.highlightBorderColor ? styleConfig?.highlightBorderColor : 'hsl(var(--foreground-default)'})`
: null,
},
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}
</SyntaxHighlighter>
{!hideCopy && (value || children) && className ? (
<div
className={[
'absolute right-2 top-2',
'opacity-0 group-hover:opacity-100 transition',
`${isDarkTheme ? 'dark' : ''}`,
].join(' ')}
>
<Button
type="default"
className="px-1.5"
icon={copied ? <Check /> : <Copy />}
onClick={() => onSelectCopy(value || children)}
>
{copied ? 'Copied' : ''}
</Button>
>
<pre>
<code className={lineNumbers ? 'grid grid-cols-[auto_1fr]' : ''}>
{lineNumbers ? (
<>
{tokens.map((line, idx) => (
<Fragment key={idx}>
<div
className={cn(
'select-none text-right text-muted bg-control px-2 min-h-5 leading-5',
idx === 0 && 'pt-6',
idx === tokens.length - 1 && 'pb-6'
)}
>
{idx + 1}
</div>
<div
className={cn(
'code-content min-h-5 leading-5 pl-6 pr-6',
idx === 0 && 'pt-6',
idx === tokens.length - 1 && 'pb-6'
)}
>
<CodeLine tokens={line} twoslash={twoslashed?.get(idx)} />
</div>
</Fragment>
))}
</>
) : (
<div className="code-content p-6">
{tokens.map((line, idx) => (
<CodeLine key={idx} tokens={line} twoslash={twoslashed?.get(idx)} />
))}
</div>
) : null}
</div>
) : (
<code className={shortCodeBlockClasses}>{value || children}</code>
)}
</>
)}
</code>
</pre>
<CodeBlockControls content={code.trim()} />
</div>
)
}
function CodeLine({
tokens: rawTokens,
twoslash,
}: {
tokens: Array<ThemedToken>
twoslash?: Map<number, Array<NodeHover>>
}) {
let offset = 0
const tokens = rawTokens.map((token) => {
const newToken = { ...token, offset }
offset += token.content.length
return newToken
})
return (
<span className="block min-h-5 leading-5">
{tokens.map((token) =>
twoslash?.has(token.offset) ? (
<AnnotatedSpan
key={token.offset}
token={token}
annotations={twoslash.get(token.offset)!}
/>
) : (
<span
key={token.offset}
style={{ color: token.color, ...getFontStyle(token.fontStyle || 0) }}
>
{token.content}
</span>
)
)}
</span>
)
}
function extractCode(children: React.ReactNode): string {
if (typeof children === 'string') return children
const child = Array.isArray(children) ? children[0] : children
if (!!child && typeof child === 'object' && 'props' in child) {
const props = child.props
if (!!props && typeof props === 'object' && 'children' in props) {
const code = props.children
if (typeof code === 'string') return code
}
}
return ''
}
function extractLang(children: React.ReactNode): BundledLanguage | null {
if (typeof children === 'string') return null
const child = Array.isArray(children) ? children[0] : children
if (!!child && typeof child === 'object' && 'props' in child) {
const props = child.props
if (!!props && typeof props === 'object' && 'className' in props) {
const className = props.className
if (typeof className === 'string') {
const lang = className.split(' ').find((className) => className.startsWith('language-'))
return lang ? tryToBundledLanguage(lang.replace('language-', '')) : null
}
}
}
return null
}
function annotationsByLine(nodes: Array<NodeHover>): Map<number, Map<number, Array<NodeHover>>> {
const result = new Map()
nodes.forEach((node) => {
const line = node.line
const char = node.character
if (!result.has(line)) {
result.set(line, new Map())
}
if (!result.get(line).has(char)) {
result.get(line).set(char, [])
}
result.get(line).get(char).push(node)
})
return result
}
function tryToBundledLanguage(lang: string): BundledLanguage | null {
if (BUNDLED_LANGUAGES.includes(lang)) {
return lang as BundledLanguage
}
return null
}