import clsx from "clsx";
import { useCallback, useState } from "react";
import SyntaxHighlighter from "react-syntax-highlighter";
import {
oneLight,
vscDarkPlus,
} from "react-syntax-highlighter/dist/esm/styles/prism";
import { useTheme } from "@/hooks/use-theme";
export interface HeadingProps extends React.ComponentProps<"h1"> {
level: 1 | 2 | 3 | 4 | 5 | 6;
}
const headingVariant = {
1: "text-2xl font-bold my-3",
2: "text-xl font-bold my-2",
3: "text-lg font-bold my-1",
4: "text-md font-bold my-1",
5: "text-base font-bold",
6: "text-base font-bold",
};
export function Heading({ children, className, ...rest }: HeadingProps) {
const { level } = rest;
return (
<h1 {...rest} className={clsx(headingVariant[level], className)}>
{children}
</h1>
);
}
export interface CodeBlockProps extends React.ComponentProps<"code"> {}
export function CodeBlock({
children,
className,
ref: _ref,
...rest
}: CodeBlockProps) {
const { isDark } = useTheme();
const match = /language-(\w+)/.exec(className || "");
const [copied, setCopied] = useState<boolean>(false);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(String(children).replace(/\n$/, ""));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}, [children]);
return match ? (
<div
className={clsx(
"w-full overflow-x-auto rounded-lg",
"bg-gray-50 dark:bg-gray-800",
)}
>
<div className="inline-flex w-full justify-between bg-gray-100 p-2">
<div className="px-2 py-1 text-xs text-gray-900 dark:text-gray-400">
{match[1]}
</div>
<button
type="button"
className="px-2 py-1 text-xs text-gray-900 dark:text-gray-400 cursor-pointer"
onClick={handleCopy}
>
{copied ? "Copied" : "Copy"}
</button>
</div>
<SyntaxHighlighter
{...rest}
PreTag="div"
language={match[1]}
style={isDark ? vscDarkPlus : oneLight}
customStyle={{
background: "transparent",
margin: 0,
padding: "1rem",
borderRadius: "0.5rem",
overflowX: "auto",
}}
codeTagProps={{
style: {
fontFamily: "monospace",
fontSize: "0.875rem",
},
}}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
</div>
) : (
<code
{...rest}
className={clsx(
"rounded-md px-1 py-0.5 text-[85%]",
"bg-gray-100 dark:bg-gray-800",
)}
>
{children}
</code>
);
}
export interface BlockQuoteProps extends React.ComponentProps<"blockquote"> {}
export function BlockQuote({ children, className, ...rest }: BlockQuoteProps) {
return (
<blockquote
{...rest}
className={clsx("border-l-4 border-gray-300 pl-4 italic", className)}
>
{children}
</blockquote>
);
}
export interface LinkProps extends React.ComponentProps<"a"> {}
const UNSAFE_HREF_PATTERN = /^(javascript|data|vbscript):/i;
export function Link({
children,
className,
href,
target,
rel,
...rest
}: LinkProps) {
const safeHref = UNSAFE_HREF_PATTERN.test(href ?? "") ? undefined : href;
const effectiveTarget = target ?? "_blank";
const relTokens = new Set([
"noopener",
"noreferrer",
...(rel?.split(/\s+/).filter(Boolean) ?? []),
]);
const safeRel = effectiveTarget === "_blank" ? [...relTokens].join(" ") : rel;
return (
<a
className={clsx(
"text-blue-600 dark:text-blue-400 hover:underline underline-offset-1",
className,
)}
href={safeHref}
target={effectiveTarget}
rel={safeRel}
{...rest}
>
{children}
</a>
);
}