CodeBlockSection.tsx84 lines · main
| 1 | import { codeToHtml } from 'shiki' |
| 2 | |
| 3 | import type { GoCodeBlockSection } from '../schemas' |
| 4 | import CodeBlockTabs from './CodeBlockTabs' |
| 5 | import { brivenDark, brivenLight } from './codeThemes' |
| 6 | |
| 7 | const lineNumberStyles = ` |
| 8 | .go-code code { counter-reset: line; } |
| 9 | .go-code code .line { counter-increment: line; } |
| 10 | .go-code code .line::before { |
| 11 | content: counter(line); |
| 12 | display: inline-block; |
| 13 | width: 2ch; |
| 14 | margin-right: 1.5ch; |
| 15 | text-align: right; |
| 16 | color: hsl(var(--foreground-light)); |
| 17 | opacity: 0.35; |
| 18 | } |
| 19 | ` |
| 20 | |
| 21 | async function highlightCode(code: string, language: string) { |
| 22 | const [darkHtml, lightHtml] = await Promise.all([ |
| 23 | codeToHtml(code, { lang: language, theme: brivenDark }), |
| 24 | codeToHtml(code, { lang: language, theme: brivenLight }), |
| 25 | ]) |
| 26 | return { darkHtml, lightHtml } |
| 27 | } |
| 28 | |
| 29 | export default async function CodeBlockSection({ section }: { section: GoCodeBlockSection }) { |
| 30 | const isMultiFile = section.files && section.files.length > 0 |
| 31 | const isSingleFileWithName = !isMultiFile && section.filename && section.code |
| 32 | |
| 33 | // Build the highlighted files array |
| 34 | const highlightedFiles = isMultiFile |
| 35 | ? await Promise.all( |
| 36 | section.files!.map(async (file) => { |
| 37 | const { darkHtml, lightHtml } = await highlightCode(file.code, file.language ?? 'sql') |
| 38 | return { filename: file.filename, darkHtml, lightHtml } |
| 39 | }) |
| 40 | ) |
| 41 | : section.code |
| 42 | ? [ |
| 43 | { |
| 44 | filename: section.filename ?? '', |
| 45 | ...(await highlightCode(section.code, section.language ?? 'sql')), |
| 46 | }, |
| 47 | ] |
| 48 | : [] |
| 49 | |
| 50 | const showTabs = |
| 51 | highlightedFiles.length > 1 || (highlightedFiles.length === 1 && isSingleFileWithName) |
| 52 | |
| 53 | return ( |
| 54 | <div className="max-w-7xl w-full min-w-0 mx-auto px-8"> |
| 55 | {(section.title || section.description) && ( |
| 56 | <div className="mb-12"> |
| 57 | {section.title && ( |
| 58 | <h2 className="text-2xl sm:text-3xl font-medium text-foreground">{section.title}</h2> |
| 59 | )} |
| 60 | {section.description && ( |
| 61 | <p className="text-foreground-lighter mt-3 text-lg">{section.description}</p> |
| 62 | )} |
| 63 | </div> |
| 64 | )} |
| 65 | <style dangerouslySetInnerHTML={{ __html: lineNumberStyles }} /> |
| 66 | <div className="go-code border border-muted rounded-xl overflow-hidden w-full"> |
| 67 | {showTabs ? ( |
| 68 | <CodeBlockTabs files={highlightedFiles} /> |
| 69 | ) : highlightedFiles.length === 1 ? ( |
| 70 | <> |
| 71 | <div |
| 72 | className="hidden dark:block px-5 py-4 sm:px-6 sm:py-5 overflow-x-auto text-[13px] leading-[1.6] [&_pre]:bg-transparent! [&_pre]:m-0! [&_code]:font-mono" |
| 73 | dangerouslySetInnerHTML={{ __html: highlightedFiles[0].darkHtml }} |
| 74 | /> |
| 75 | <div |
| 76 | className="block dark:hidden px-5 py-4 sm:px-6 sm:py-5 overflow-x-auto text-[13px] leading-[1.6] [&_pre]:bg-transparent! [&_pre]:m-0! [&_code]:font-mono" |
| 77 | dangerouslySetInnerHTML={{ __html: highlightedFiles[0].lightHtml }} |
| 78 | /> |
| 79 | </> |
| 80 | ) : null} |
| 81 | </div> |
| 82 | </div> |
| 83 | ) |
| 84 | } |