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) => (
{cell}
|
))}
{rows.map((row, rIndex) => (
{row.map((cell, cIndex) => (
{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);
}