"use client";
import React, { useState } from "react";
import { Check, Copy } from "lucide-react";
import ReactMarkdown from "react-markdown";
import type { PluggableList } from "unified";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { extractMarkdownHeadings } from "@/lib/markdown-headings";
import { cn } from "@/lib/utils";
type MarkdownSize = "sm" | "base" | "lg";
type MarkdownProps = {
content?: string;
allowHtml?: boolean;
className?: string;
dir?: "rtl" | "ltr";
justify?: boolean;
size?: MarkdownSize;
};
function CodeBlock({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
const [copied, setCopied] = useState(false);
const language = /language-([\w-]+)/.exec(className || "")?.[1] || "text";
const code = String(children).replace(/\n$/, "");
const copyCode = async () => {
if (!navigator.clipboard) return;
await navigator.clipboard.writeText(code);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
};
return (
{language}
{code}
);
}
export default function Markdown({
content = "",
allowHtml = false,
className = "",
dir = "rtl",
justify = false,
size = "sm",
}: MarkdownProps) {
const rehypePlugins: PluggableList | undefined = allowHtml ? [rehypeRaw, rehypeSanitize] : undefined;
const headings = extractMarkdownHeadings(content);
let headingIndex = 0;
const baseSizeClass =
size === "sm" ? "text-sm" : size === "lg" ? "text-lg" : "text-base";
const hScale =
size === "sm"
? { h1: "text-xl", h2: "text-lg", h3: "text-base", h4: "text-base" }
: size === "base"
? { h1: "text-3xl", h2: "text-2xl", h3: "text-xl", h4: "text-lg" }
: { h1: "text-4xl", h2: "text-3xl", h3: "text-2xl", h4: "text-xl" };
const justifyStyle: React.CSSProperties | undefined = justify
? { textAlign: "justify", textJustify: "inter-word" }
: undefined;
const nextHeadingId = (level: 1 | 2 | 3) => {
while (headingIndex < headings.length) {
const heading = headings[headingIndex];
headingIndex += 1;
if (heading.level === level) return heading.id;
}
return undefined;
};
return (
,
h2: (p) => ,
h3: (p) => ,
h4: (p) => ,
p: (p) => ,
a: (p) => ,
ul: (p) => ,
ol: (p) =>
,
li: (p) => ,
hr: (p) =>
,
blockquote: (p) => (
),
code: ({ className, children, node, ...p }) => {
const isInline =
node?.tagName === "code" &&
!/language-/.test(className || "") &&
!String(children).includes("\n");
if (isInline) {
return (
{children}
);
}
return {children};
},
pre: ({ children }) => <>{children}>,
table: (p) => (
),
th: (p) => | ,
td: (p) => | ,
}}
>
{content}
);
}