Repository Public
1import { memo } from 'react';2import { parseMarkdown, type Inline } from '@/lib/markdown';3import { cn } from '@/lib/cn';45function Inl({ parts }: { parts: Inline[] }) {6 return (7 <>8 {parts.map((p, i) =>9 p.t === 'code' ? (10 <code key={i} className="rounded-[5px] bg-surface-2 px-1 py-px font-mono text-[0.9em] text-text">{p.v}</code>11 ) : p.t === 'bold' ? (12 <strong key={i} className="font-semibold text-text">{p.v}</strong>13 ) : p.t === 'italic' ? (14 <em key={i}>{p.v}</em>15 ) : (16 <span key={i}>{p.v}</span>17 ),18 )}19 </>20 );21}2223/**24 * Model text. Everything is a React text node: URLs stay plain text (never links), image syntax25 * is shown as written (never fetched), code blocks are escaped.26 */27export const Markdown = memo(function Markdown({ text, className }: { text: string; className?: string }) {28 const blocks = parseMarkdown(text);29 return (30 <div className={cn('flex min-w-0 flex-col gap-2.5 break-words text-[14px] leading-[1.65] text-text [overflow-wrap:anywhere]', className)} data-testid="md">31 {blocks.map((b, i) => {32 switch (b.t) {33 case 'p':34 return <p key={i} className="whitespace-pre-wrap"><Inl parts={b.inl} /></p>;35 case 'h':36 return (37 <p key={i} role="heading" aria-level={b.level + 2} className={cn('font-semibold tracking-tight text-text', b.level === 1 ? 'text-[16px]' : b.level === 2 ? 'text-[15px]' : 'text-[14px]')}>38 <Inl parts={b.inl} />39 </p>40 );41 case 'ul':42 return (43 <ul key={i} className="flex list-disc flex-col gap-1 pl-5 marker:text-faint">44 {b.items.map((it, j) => <li key={j} className="whitespace-pre-wrap pl-0.5"><Inl parts={it} /></li>)}45 </ul>46 );47 case 'ol':48 return (49 <ol key={i} start={b.start} className="tnum flex list-decimal flex-col gap-1 pl-6 marker:text-muted">50 {b.items.map((it, j) => <li key={j} className="whitespace-pre-wrap pl-0.5"><Inl parts={it} /></li>)}51 </ol>52 );53 case 'quote':54 return <blockquote key={i} className="whitespace-pre-wrap border-l-2 border-line-3 pl-3 text-text-2"><Inl parts={b.inl} /></blockquote>;55 case 'code':56 return (57 <pre key={i} data-lang={b.lang ?? undefined} className="overflow-x-auto rounded-sm border border-line bg-surface-2 px-3 py-2 font-mono text-[12.5px] leading-relaxed text-text">58 <code>{b.text}</code>59 </pre>60 );61 case 'hr':62 return <hr key={i} className="border-line-2" />;63 }64 })}65 </div>66 );67});68