import { Box, Code, Divider, Heading, Link, List, ListItem, OrderedList, Table, Tbody, Td, Text, Th, Thead, Tr, useColorModeValue, } from '@chakra-ui/react'; import katex from 'katex'; import { marked } from 'marked'; import markedKatex from 'marked-katex-extension'; import React from 'react'; import CodeBlock from '../../code/CodeBlock'; import ImageWithFallback from '../../markdown/ImageWithFallback'; import domPurify from '../lib/domPurify'; try { if (localStorage) { marked.use( markedKatex({ nonStandard: false, displayMode: true, throwOnError: false, strict: true, colorIsTextColor: true, errorColor: 'red', }), ); } } catch (_) { // Silently ignore errors in marked setup - fallback to default behavior } const MemoizedCodeBlock = React.memo(CodeBlock); /** * Utility to map heading depth to Chakra heading styles that * roughly match typical markdown usage. */ const getHeadingProps = (depth: number) => { switch (depth) { case 1: return { as: 'h1', size: 'xl', mt: 4, mb: 2 }; case 2: return { as: 'h2', size: 'lg', mt: 3, mb: 2 }; case 3: return { as: 'h3', size: 'md', mt: 2, mb: 1 }; case 4: return { as: 'h4', size: 'sm', mt: 2, mb: 1 }; case 5: return { as: 'h5', size: 'sm', mt: 2, mb: 1 }; case 6: return { as: 'h6', size: 'xs', mt: 2, mb: 1 }; default: return { as: `h${depth}`, size: 'md', mt: 2, mb: 1 }; } }; interface TableToken extends marked.Tokens.Table { align: Array<'center' | 'left' | 'right' | null>; header: (string | marked.Tokens.TableCell)[]; rows: (string | marked.Tokens.TableCell)[][]; } const CustomHeading: React.FC<{ text: string; depth: number }> = ({ text, depth }) => { const headingProps = getHeadingProps(depth); return ( {text} ); }; const CustomParagraph: React.FC<{ children: React.ReactNode }> = ({ children }) => { return ( {children} ); }; const CustomBlockquote: React.FC<{ children: React.ReactNode }> = ({ children }) => { return ( {children} ); }; const CustomCodeBlock: React.FC<{ code: string; language?: string }> = ({ code, language }) => { return ( Promise.resolve()} /> ); }; const CustomHr: React.FC = () => ; const CustomList: React.FC<{ ordered?: boolean; start?: number; children: React.ReactNode; }> = ({ ordered, start, children }) => { const commonStyles = { fontSize: 'sm', wordBreak: 'break-word' as const, maxWidth: '100%' as const, stylePosition: 'outside' as const, mb: 2, pl: 4, }; return ordered ? ( {children} ) : ( {children} ); }; const CustomListItem: React.FC<{ children: React.ReactNode; }> = ({ children }) => { return {children}; }; const CustomKatex: React.FC<{ math: string; displayMode: boolean }> = ({ math, displayMode }) => { const renderedMath = katex.renderToString(math, { displayMode }); return ( ); }; const CustomTable: React.FC<{ header: React.ReactNode[]; align: Array<'center' | 'left' | 'right' | null>; rows: React.ReactNode[][]; }> = ({ header, align, rows }) => { return ( {header.map((cell, i) => ( ))} {rows.map((row, rIndex) => ( {row.map((cell, cIndex) => ( ))} ))}
{cell}
{cell}
); }; const CustomHtmlBlock: React.FC<{ content: string }> = ({ content }) => { return ; }; const CustomText: React.FC<{ text: React.ReactNode }> = ({ text }) => { return ( {text} ); }; interface CustomStrongProps { children: React.ReactNode; } const CustomStrong: React.FC = ({ children }) => { return {children}; }; const CustomEm: React.FC<{ children: React.ReactNode }> = ({ children }) => { return ( {children} ); }; const CustomDel: React.FC<{ text: string }> = ({ text }) => { return ( {text} ); }; const CustomCodeSpan: React.FC<{ code: string }> = ({ code }) => { const bg = useColorModeValue('gray.100', 'gray.800'); return ( {code} ); }; const CustomMath: React.FC<{ math: string; displayMode?: boolean }> = ({ math, displayMode = false, }) => { return ( {math} ); }; const CustomLink: React.FC<{ href: string; title?: string; children: React.ReactNode; }> = ({ href, title, children, ...props }) => { return ( {children} ); }; const CustomImage: React.FC<{ href: string; text: string; title?: string }> = ({ href, text, title, }) => { return ( ); }; /** * A helper function that iterates through a list of Marked tokens * and returns an array of React elements. This is the heart of the * custom-rendering logic, used both top-level and for nested tokens. */ function parseTokens(tokens: marked.Token[]): JSX.Element[] { const output: JSX.Element[] = []; let blockquoteContent: JSX.Element[] = []; tokens.forEach((token, i) => { switch (token.type) { case 'heading': output.push(); break; case 'paragraph': { const parsedContent = token.tokens ? parseTokens(token.tokens) : token.text; if (blockquoteContent.length > 0) { blockquoteContent.push({parsedContent}); } else { output.push({parsedContent}); } break; } case 'br': output.push(
); break; case 'escape': { break; } case 'blockquote_start': blockquoteContent = []; break; case 'blockquote_end': output.push({parseTokens(blockquoteContent)}); blockquoteContent = []; break; case 'blockquote': { output.push( {token.tokens ? parseTokens(token.tokens) : null} , ); break; } case 'math': output.push(); break; case 'inlineMath': output.push(); break; case 'inlineKatex': case 'blockKatex': { const katexToken = token as any; output.push( , ); break; } case 'code': output.push(); break; case 'hr': output.push(); break; case 'list': { const { ordered, start, items } = token; const listItems = items.map((listItem, idx) => { const nestedContent = parseTokens(listItem.tokens); return {nestedContent}; }); output.push( {listItems} , ); break; } case 'table': { const tableToken = token as TableToken; output.push( typeof cell === 'string' ? cell : parseTokens(cell.tokens || []), )} align={tableToken.align} rows={tableToken.rows.map(row => row.map(cell => (typeof cell === 'string' ? cell : parseTokens(cell.tokens || []))), )} />, ); break; } case 'html': output.push(); break; case 'def': case 'space': break; case 'strong': output.push({parseTokens(token.tokens || [])}); break; case 'em': output.push( {token.tokens ? parseTokens(token.tokens) : token.text}, ); break; case 'codespan': output.push(); break; case 'link': output.push( {token.tokens ? parseTokens(token.tokens) : token.text} , ); break; case 'image': output.push( , ); break; case 'text': { const parsedContent = token.tokens ? parseTokens(token.tokens) : token.text; if (blockquoteContent.length > 0) { blockquoteContent.push({parsedContent}); } else { output.push(); } break; } default: console.warn('Unhandled token type:', token.type, token); } }); return output; } export function renderMessageMarkdown(markdown: string): JSX.Element[] { marked.setOptions({ breaks: true, gfm: true, silent: false, async: true, }); const tokens = marked.lexer(domPurify(markdown)); return parseTokens(tokens); }