111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import React from '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';
|
|
|
|
type MarkdownSize = 'sm' | 'base' | 'lg';
|
|
|
|
type MarkdownProps = {
|
|
content?: string;
|
|
allowHtml?: boolean;
|
|
className?: string;
|
|
dir?: 'rtl' | 'ltr';
|
|
justify?: boolean;
|
|
size?: MarkdownSize;
|
|
};
|
|
|
|
export default function Markdown({
|
|
content = '',
|
|
allowHtml = false,
|
|
className = '',
|
|
dir = 'rtl',
|
|
justify = false,
|
|
size = 'sm',
|
|
}: MarkdownProps) {
|
|
const rehypePlugins: PluggableList | undefined = allowHtml ? [rehypeRaw, rehypeSanitize] : undefined;
|
|
|
|
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;
|
|
|
|
return (
|
|
<div
|
|
dir={dir}
|
|
className={`markdown-body ${baseSizeClass} text-right leading-7 break-words ${className}`}
|
|
style={justifyStyle}
|
|
>
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
rehypePlugins={rehypePlugins}
|
|
components={{
|
|
h1: (p) => <h1 className={`mt-6 font-bold ${hScale.h1}`} {...p} />,
|
|
h2: (p) => <h2 className={`mt-6 font-bold ${hScale.h2}`} {...p} />,
|
|
h3: (p) => <h3 className={`mt-5 font-semibold ${hScale.h3}`} {...p} />,
|
|
h4: (p) => <h4 className={`mt-4 font-semibold ${hScale.h4}`} {...p} />,
|
|
p: (p) => <p className="my-3" {...p} />,
|
|
a: (p) => <a className="underline decoration-primary hover:opacity-90 break-all" target="_blank" rel="noopener noreferrer" {...p} />,
|
|
ul: (p) => <ul className="my-3 list-disc ps-6 space-y-1.5" {...p} />,
|
|
ol: (p) => <ol className="my-3 list-decimal ps-6 space-y-1.5" {...p} />,
|
|
li: (p) => <li className="[&>ul]:my-1.5 [&>ol]:my-1.5" {...p} />,
|
|
hr: (p) => <hr className="my-5 border-muted" {...p} />,
|
|
blockquote: (p) => (
|
|
<blockquote className="my-3 border-r-4 pr-4 italic text-muted-foreground" {...p} />
|
|
),
|
|
code: ({ className, children, node, ...p }) => {
|
|
const isInline =
|
|
node?.tagName === 'code' &&
|
|
!/language-/.test(className || '') &&
|
|
!String(children).includes('\n');
|
|
|
|
if (isInline) {
|
|
return (
|
|
<code className="rounded bg-muted px-1 py-0.5 text-[0.9em]" {...p}>
|
|
{children}
|
|
</code>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<code className={className} {...p}>
|
|
{children}
|
|
</code>
|
|
);
|
|
},
|
|
pre: ({ className = '', children, ...p }) => (
|
|
<pre
|
|
className={[
|
|
"my-4 overflow-x-auto rounded-md bg-muted p-4 text-[0.9em]",
|
|
className,
|
|
].filter(Boolean).join(" ")}
|
|
{...p}
|
|
>
|
|
{children}
|
|
</pre>
|
|
),
|
|
table: (p) => (
|
|
<div className="my-3 overflow-x-auto">
|
|
<table className="w-full border-collapse" {...p} />
|
|
</div>
|
|
),
|
|
th: (p) => <th className="border-b p-2 text-right font-semibold" {...p} />,
|
|
td: (p) => <td className="border-b p-2 align-top" {...p} />,
|
|
}}
|
|
>
|
|
{content}
|
|
</ReactMarkdown>
|
|
</div>
|
|
);
|
|
}
|