1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 20:15:31 -04:00
Files
jarv.is/components/code-block.tsx
T

76 lines
2.9 KiB
TypeScript

import { codeToHtml } from "shiki";
import { cacheLife } from "next/cache";
import { CopyButton } from "@/components/copy-button";
import { cn } from "@/lib/utils";
/**
* Recursively extracts plain text content from React nodes.
* Replacement for the `react-to-text` package.
*/
const getTextContent = (node: React.ReactNode): string => {
if (node == null || typeof node === "boolean") return "";
if (typeof node === "string" || typeof node === "number") return String(node);
if (Array.isArray(node)) return node.map(getTextContent).join("");
if (typeof node === "object" && "props" in node) {
return getTextContent((node as React.ReactElement<{ children?: React.ReactNode }>).props.children);
}
return "";
};
interface CodeBlockProps extends React.ComponentProps<"pre"> {
showLineNumbers?: boolean;
}
const renderCode = async (code: string, lang: string): Promise<string> => {
"use cache";
cacheLife("max");
return codeToHtml(code, {
lang,
themes: { light: "github-light", dark: "github-dark" },
});
};
const CodeBlock = async ({ children, className, showLineNumbers = true, ...props }: CodeBlockProps) => {
// Escape hatch for non-code pre blocks
if (!children || typeof children !== "object" || !("props" in children)) {
return (
<pre className={className} {...props}>
{children}
</pre>
);
}
const codeProps = children.props as React.ComponentProps<"code">;
const codeString = getTextContent(codeProps.children).trim();
const lang = codeProps.className?.split("language-")[1] ?? "text";
const html = await renderCode(codeString, lang);
return (
<div className="group not-prose relative">
<CopyButton
value={codeString}
className="absolute top-2 right-2 z-10 opacity-0 transition-opacity group-hover:opacity-100"
/>
<div
data-slot="code-block"
data-lang={lang}
data-line-numbers={showLineNumbers || undefined}
className={cn(
"bg-code text-code-foreground overflow-x-auto overflow-y-hidden rounded-xl text-[13px] leading-normal outline-none",
"[&_span]:!bg-transparent [&_span[style*='color']]:dark:!text-(--shiki-dark)",
"[&_pre]:m-0 [&_pre]:rounded-xl [&_pre]:!bg-transparent",
"[&_code]:white-space-pre [&_code]:grid [&_code]:min-w-full [&_code]:px-4 [&_code]:py-3.5 [&_code]:[counter-reset:line]",
"[&_.line]:min-h-1lh [&_.line]:inline-block [&_.line]:w-full [&_.line]:py-0.5",
"data-[line-numbers]:[&_.line]:before:text-code-number data-[line-numbers]:[&_.line]:before:mr-6 data-[line-numbers]:[&_.line]:before:inline-block data-[line-numbers]:[&_.line]:before:w-5 data-[line-numbers]:[&_.line]:before:text-right data-[line-numbers]:[&_.line]:before:content-[counter(line)] data-[line-numbers]:[&_.line]:before:[counter-increment:line]",
className
)}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
};
export { CodeBlock };