import { type CSSProperties, type ReactNode } from 'react'; import { marked, type Token, type Tokens } from 'marked'; // Front components run in a remote-DOM sandbox: raw HTML injection is not // allowed, so Markdown is rendered as real React elements with inline styles. const INK = '#1f2430'; const HEADING = '#10152a'; const ACCENT = '#1961ed'; const styles: Record = { h1: { fontSize: '22px', fontWeight: 700, color: HEADING, margin: '28px 0 12px', lineHeight: 1.3 }, h2: { fontSize: '18px', fontWeight: 700, color: HEADING, margin: '24px 0 10px', lineHeight: 1.3 }, h3: { fontSize: '15px', fontWeight: 700, color: HEADING, margin: '20px 0 8px', lineHeight: 1.3 }, p: { margin: '0 0 14px', lineHeight: 1.7, color: INK }, ul: { margin: '0 0 14px', paddingLeft: '22px' }, ol: { margin: '0 0 14px', paddingLeft: '22px' }, li: { margin: '4px 0', lineHeight: 1.6, color: INK }, a: { color: ACCENT, textDecoration: 'none' }, code: { fontFamily: "'SFMono-Regular', Menlo, monospace", fontSize: '0.9em', background: '#f1f3f9', padding: '2px 6px', borderRadius: '4px', }, blockquote: { margin: '14px 0', padding: '4px 18px', borderLeft: `3px solid ${ACCENT}`, background: '#f6f8fd', color: '#47506a', }, hr: { border: 0, borderTop: '1px solid #e6e9f2', margin: '24px 0' }, pre: { background: '#f1f3f9', padding: '14px 16px', borderRadius: '6px', overflowX: 'auto', fontFamily: "'SFMono-Regular', Menlo, monospace", fontSize: '12px', lineHeight: 1.5, margin: '0 0 14px', }, }; const BLOCK_TOKEN_TYPES = new Set([ 'list', 'paragraph', 'code', 'blockquote', 'heading', 'hr', 'space', ]); const renderInline = (tokens: Token[] | undefined, keyPrefix: string): ReactNode[] => { if (!tokens) return []; return tokens.map((token, index): ReactNode => { const key = `${keyPrefix}-${index}`; switch (token.type) { case 'strong': return {renderInline((token as Tokens.Strong).tokens, key)}; case 'em': return {renderInline((token as Tokens.Em).tokens, key)}; case 'codespan': return {(token as Tokens.Codespan).text}; case 'br': return
; case 'link': { const link = token as Tokens.Link; const safe = /^(https?:|mailto:)/i.test(link.href ?? ''); return safe ? ( {renderInline(link.tokens, key)} ) : ( {renderInline(link.tokens, key)} ); } default: { const nested = (token as Tokens.Text).tokens; if (Array.isArray(nested) && nested.length > 0) { return {renderInline(nested, key)}; } return {(token as Tokens.Text).text ?? ''}; } } }); }; const renderBlocks = (tokens: Token[]): ReactNode[] => tokens.map((token, index): ReactNode => { const key = `b-${index}`; switch (token.type) { case 'heading': { const heading = token as Tokens.Heading; const Tag = (['h1', 'h2', 'h3', 'h3'][heading.depth - 1] ?? 'h3') as 'h1' | 'h2' | 'h3'; return ( {renderInline(heading.tokens, key)} ); } case 'paragraph': return

{renderInline((token as Tokens.Paragraph).tokens, key)}

; case 'list': { const list = token as Tokens.List; const items = list.items.map((item, itemIndex) => { // List items can hold block-level tokens (nested lists, extra // paragraphs, code). Render those as blocks; keep simple items inline. const hasBlock = item.tokens?.some((child) => BLOCK_TOKEN_TYPES.has(child.type)); return (
  • {hasBlock ? renderBlocks(item.tokens) : renderInline(item.tokens, `${key}-${itemIndex}`)}
  • ); }); return list.ordered ? (
      {items}
    ) : ( ); } case 'blockquote': return (
    {renderBlocks((token as Tokens.Blockquote).tokens)}
    ); case 'code': return
    {(token as Tokens.Code).text}
    ; case 'hr': return
    ; case 'space': return null; case 'text': { // Loose list items expose their content as a `text` block token with // nested inline tokens — render those so bold/italic/links survive. const textToken = token as Tokens.Text; return ( {textToken.tokens ? renderInline(textToken.tokens, key) : textToken.text} ); } default: { const text = (token as { text?: string }).text; return text ?

    {text}

    : null; } } }); export const Markdown = ({ content }: { content: string }): ReactNode => (
    {renderBlocks(marked.lexer(content))}
    );