Files
guilan-ace-frontend/src/components/Markdown.tsx
Amirhossein Khalili dacbd3a328
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
initial commit
2026-05-19 20:58:15 +03:30

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>
);
}