2026-05-31 12:41:10 +02:00
|
|
|
import { codeToHtml } from "shiki"
|
2025-02-18 11:08:04 +01:00
|
|
|
import { cn } from "@/lib/utils"
|
2026-05-31 12:41:10 +02:00
|
|
|
import { CopyButton } from "./CopyButton"
|
2025-02-14 10:54:09 +01:00
|
|
|
|
|
|
|
|
interface CopyableCodeProps {
|
|
|
|
|
code: string
|
2025-02-18 11:08:04 +01:00
|
|
|
language?: string
|
|
|
|
|
className?: string
|
2025-02-14 10:54:09 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 12:41:10 +02:00
|
|
|
/**
|
|
|
|
|
* Server-rendered code block with Shiki syntax highlighting.
|
|
|
|
|
*
|
|
|
|
|
* Shiki runs at build time (Next.js static export pre-renders every
|
|
|
|
|
* page) so the resulting HTML carries pre-coloured `<span>` elements
|
|
|
|
|
* and the client doesn't have to load any highlighter JS. The copy
|
|
|
|
|
* button is the only interactive bit and lives in CopyButton, a tiny
|
|
|
|
|
* client component.
|
|
|
|
|
*
|
|
|
|
|
* Default theme is `github-dark` — matches the Hermes/Docusaurus look
|
|
|
|
|
* the user asked us to emulate. Default language is bash because most
|
|
|
|
|
* snippets in the docs are shell commands.
|
|
|
|
|
*
|
|
|
|
|
* Defensive fallback: if Shiki can't tokenize the requested language
|
|
|
|
|
* (unknown alias, unsupported grammar) we fall back to a plain
|
|
|
|
|
* dark-background <pre> so the page never crashes.
|
|
|
|
|
*/
|
|
|
|
|
const CopyableCode = async ({ code, language = "bash", className }: CopyableCodeProps) => {
|
|
|
|
|
let html: string
|
|
|
|
|
try {
|
|
|
|
|
html = await codeToHtml(code, {
|
|
|
|
|
lang: language,
|
|
|
|
|
theme: "github-dark",
|
|
|
|
|
})
|
|
|
|
|
} catch {
|
|
|
|
|
// Unknown lang or grammar error → render as plain text on a dark
|
|
|
|
|
// background to preserve the visual style without colour.
|
|
|
|
|
const escaped = code
|
|
|
|
|
.replace(/&/g, "&")
|
|
|
|
|
.replace(/</g, "<")
|
|
|
|
|
.replace(/>/g, ">")
|
|
|
|
|
html = `<pre class="shiki" style="background-color:#24292e;color:#e1e4e8"><code>${escaped}</code></pre>`
|
2025-02-14 10:54:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-31 12:41:10 +02:00
|
|
|
<div className={cn("relative w-full my-4", className)}>
|
|
|
|
|
<div
|
2025-02-18 11:08:04 +01:00
|
|
|
className={cn(
|
2026-05-31 12:41:10 +02:00
|
|
|
"rounded-md overflow-hidden",
|
|
|
|
|
"[&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:text-sm [&_pre]:leading-relaxed",
|
|
|
|
|
"[&_code]:font-mono",
|
2025-02-18 11:08:04 +01:00
|
|
|
)}
|
2026-05-31 12:41:10 +02:00
|
|
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
|
|
|
/>
|
|
|
|
|
<CopyButton text={code} />
|
2025-02-14 10:54:09 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default CopyableCode
|