"use client" import React, { useCallback } from "react" import ReactMarkdown, { Components } from "react-markdown" import remarkGfm from "remark-gfm" import katex from "katex" import "katex/dist/katex.min.css" import rehypeRaw from "rehype-raw" import { TableRowCard } from "../cards/table-row-card" import { Accordion } from "./accordion" import Prism from "prismjs" import "prismjs/themes/prism-tomorrow.css" import "prismjs/components/prism-javascript" import "prismjs/components/prism-typescript" import "prismjs/components/prism-jsx" import "prismjs/components/prism-tsx" import "prismjs/components/prism-json" import "prismjs/components/prism-bash" import "prismjs/components/prism-css" import "prismjs/components/prism-markdown" import "prismjs/components/prism-yaml" import "prismjs/components/prism-python" import "prismjs/components/prism-rust" import "prismjs/components/prism-solidity" const SCROLL_OFFSET = 150 const scrollToElementWithOffset = (target: HTMLElement) => { const rect = target.getBoundingClientRect() const targetPosition = rect.top + window.pageYOffset - SCROLL_OFFSET // Set margin before scrolling target.style.scrollMarginTop = `${SCROLL_OFFSET}px` requestAnimationFrame(() => { window.scrollTo({ top: targetPosition, behavior: "smooth", }) }) } interface CustomComponents extends Components { "table-row-card": React.ComponentType<{ node: any children: string }> accordion: React.ComponentType<{ node: any children: string }> "footnote-ref": React.ComponentType<{ identifier: string label: string }> "footnote-definition": React.ComponentType<{ identifier: string label: string children: React.ReactNode }> footnotes: React.ComponentType<{ children: React.ReactNode }> } const generateSectionId = (text: string) => { return text .toLowerCase() .replace(/[']/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") } export const createMarkdownElement = ( tag: keyof JSX.IntrinsicElements, props: any ) => React.createElement(tag, { ...props, ref: (node: HTMLElement | null) => { if (node && node.textContent) { node.setAttribute( "data-section-id", generateSectionId(node.textContent) ) } }, }) const Table = (props: any) => { return (
{props.children}
) } const TableRow = (props: any) => { return {props.children} } const TableHead = (props: any) => { if (!props.children || props.children.length === 0) { return null } const isEmpty = React.Children.toArray(props.children).every((child: any) => { if (!child.props || !child.props.children) return true if (child.props.children) { const thChildren = React.Children.toArray(child.props.children) if (thChildren.length === 0) return true return thChildren.every((thChild: any) => { if (!thChild) return true if (typeof thChild === "string") return thChild.trim() === "" if (!thChild.props || !thChild.props.children) return true if (typeof thChild.props.children === "string") return thChild.props.children.trim() === "" return false }) } return true }) if (isEmpty) { return null } return {props.children} } const TableBody = (props: any) => { if (!props.children || props.children.length === 0) { return null } return {props.children} } const remarkCustomNewlines = () => { return (tree: any) => { const visit = (node: any) => { if (node.type === "text" && typeof node.value === "string") { if (node.value.includes("/n")) { const parts = node.value.split("/n") const newChildren: any[] = [] parts.forEach((part: string, index: number) => { newChildren.push({ type: "text", value: part }) if (index < parts.length - 1) { newChildren.push({ type: "break" }) } }) return newChildren } } if (node.children) { const newChildren: any[] = [] for (const child of node.children) { const result = visit(child) if (Array.isArray(result)) { newChildren.push(...result) } else if (result) { newChildren.push(result) } else { newChildren.push(child) } } node.children = newChildren } return node } return visit(tree) } } // Custom math components const KaTeXBlock = ({ math }: { math: string }) => { const mathRef = React.useRef(null) React.useEffect(() => { if (mathRef.current) { try { const processedMath = preprocessMathContent(math.trim()) katex.render(processedMath, mathRef.current, { displayMode: true, throwOnError: false, }) } catch (error) { console.error("KaTeX block render error:", error) if (mathRef.current) { mathRef.current.textContent = math } } } }, [math]) return
} const KaTeXInline = ({ math }: { math: string }) => { const mathRef = React.useRef(null) React.useEffect(() => { if (mathRef.current) { try { // Pre-process the math content to replace "mod" with "\pmod" const processedMath = preprocessMathContent(math.trim()) katex.render(processedMath, mathRef.current, { displayMode: false, throwOnError: false, }) } catch (error) { console.error("KaTeX inline render error:", error) if (mathRef.current) { mathRef.current.textContent = math } } } }, [math]) return } // Preprocess math content to improve rendering const preprocessMathContent = (mathContent: string): string => { // Replace pattern "x ; mod ; q" or "x \mod q" with "\pmod{q}" const modRegex = /([^\\])(;?\s*mod\s*;?\s*)([^{])/g const processedMath = mathContent.replace( modRegex, (match, before, mod, after) => { return `${before}\\pmod{${after}}` } ) // Also handle cases like "\mod q" or "\mod{q}" const modCommandRegex = /\\mod\s+([^{])/g return processedMath.replace(modCommandRegex, (match, after) => { return `\\pmod{${after}}` }) } // Define a simple regex to extract math content from our markdown const extractMathBlocks = (content: string) => { const mathBlocks: { start: number end: number content: string isBlock: boolean }[] = [] const blockMathRegex = /\$\$([\s\S]*?)\$\$/g let match: RegExpExecArray | null while ((match = blockMathRegex.exec(content)) !== null) { mathBlocks.push({ start: match.index, end: match.index + match[0].length, content: match[1], isBlock: true, }) } const inlineMathRegex = /\$(.*?)\$/g let inlineMatch: RegExpExecArray | null while ((inlineMatch = inlineMathRegex.exec(content)) !== null) { // Make sure this isn't already part of a block math section const isInsideBlockMath = mathBlocks.some( (block) => inlineMatch!.index > block.start && inlineMatch!.index < block.end ) if (!isInsideBlockMath) { mathBlocks.push({ start: inlineMatch.index, end: inlineMatch.index + inlineMatch[0].length, content: inlineMatch[1], isBlock: false, }) } } mathBlocks.sort((a, b) => a.start - b.start) return mathBlocks } // Helper to check if text contains unescaped math delimiters const containsMath = (text: string): boolean => { // Skip markdown links if (text.match(/\[.*?\]\(.*?\)/)) { return false } if (!text.includes("$")) return false // Check for currency pattern first const currencyPattern = /\$\s*\d+(?:,\d{3})*(?:\.\d{2})?(?!\^|\{|\}|\d)/g if (text.match(currencyPattern) && !text.match(/\$.*[\^_{}].*\$/)) { return false } const blockMathRegex = /\$\$([\s\S]*?)\$\$/g const inlineMathRegex = /(? { const parts: React.ReactNode[] = [] let lastIndex = 0 try { // First handle block math ($$...$$) const blockMathRegex = /\$\$([\s\S]*?)\$\$/g let match: RegExpExecArray | null while ((match = blockMathRegex.exec(text))) { if (match.index > lastIndex) { parts.push(text.slice(lastIndex, match.index)) } const mathContent = match[1].trim() parts.push( ) lastIndex = match.index + match[0].length } // Then handle remaining text for inline math ($...$) const remainingText = text.slice(lastIndex) if (remainingText) { const inlineParts: React.ReactNode[] = [] let inlineLastIndex = 0 const inlineMathRegex = /(? inlineLastIndex) { inlineParts.push( remainingText.slice(inlineLastIndex, inlineMatch.index) ) } const mathContent = inlineMatch[1].trim() inlineParts.push( ) inlineLastIndex = inlineMatch.index + inlineMatch[0].length } if (inlineLastIndex < remainingText.length) { inlineParts.push(remainingText.slice(inlineLastIndex)) } parts.push(...inlineParts) } return <>{parts} } catch (error) { console.error("Error processing text with math:", error) return <>{text} } } const rehypeProcessBrTags = () => { return (tree: any) => { const visit = (node: any) => { if ( node.type === "element" && (node.tagName === "td" || node.tagName === "th") ) { // Look for text nodes that contain \n or
and convert them if (node.children) { for (let i = 0; i < node.children.length; i++) { const child = node.children[i] if (child.type === "text" && child.value) { if (child.value.includes("\n")) { const parts = child.value.split("\n") const newChildren: any[] = [] parts.forEach((part: string, index: number) => { newChildren.push({ type: "text", value: part }) // Add
element except after the last part if (index < parts.length - 1) { newChildren.push({ type: "element", tagName: "br", properties: {}, children: [], }) } }) node.children.splice(i, 1, ...newChildren) // Adjust i based on the new length of children we've inserted i += newChildren.length - 1 } } } } } if (node.children) { node.children.forEach(visit) } return node } return visit(tree) } } const CodeBlock = ({ className, children, }: { className?: string children: string }) => { const language = className ? className.replace(/language-/, "") : "text" const codeRef = React.useRef(null) React.useEffect(() => { if (codeRef.current) { Prism.highlightElement(codeRef.current) } }, [children]) return (
      
        {children}
      
    
) } const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({ a: ({ href, children }) => { if (href?.startsWith("#")) { return ( { e.preventDefault() const targetId = href.slice(1) const target = document.getElementById(targetId) if (target) { scrollToElementWithOffset(target) } }} className="text-anakiwa-500 hover:text-orange duration-200" > {children} ) } return ( {children} ) }, h1: ({ ...props }) => createMarkdownElement("h1", { className: "text-primary text-4xl md:text-5xl font-bold", ...props, }), h2: ({ ...props }) => createMarkdownElement("h2", { className: "text-primary text-4xl", ...props, }), h3: ({ ...props }) => createMarkdownElement("h3", { className: "text-primary text-3xl", ...props, }), h4: ({ ...props }) => createMarkdownElement("h4", { className: "text-primary text-xl", ...props, }), h5: ({ ...props }) => createMarkdownElement("h5", { className: "text-primary text-lg font-bold", ...props, }), h6: ({ ...props }) => createMarkdownElement("h6", { className: "text-primary text-md font-bold", ...props, }), code: ({ node, inline, className, children, ...props }) => { const match = /language-(\w+)/.exec(className || "") return !inline ? ( {String(children).replace(/\n$/, "")} ) : ( {children} ) }, p: ({ node, children }: { node: any; children: React.ReactNode }) => { const childArray = React.Children.toArray(children) const isOnlyLink = childArray.length === 1 && React.isValidElement(childArray[0]) && childArray[0].type === "a" if (isOnlyLink) { return <>{children} } // For other paragraphs, continue with normal processing const text = childArray .map((child) => { if (typeof child === "string") return child if (React.isValidElement(child) && child.props?.children) { return child.props.children } return "" }) .join("") let isMathOnly = false if (text.trim().startsWith("$$") && text.trim().endsWith("$$")) { const innerContent = text.trim().slice(2, -2) if (!innerContent.includes("$$")) { isMathOnly = true } } if (containsMath(text)) { return (

) } return (

{children}

) }, // Handle math in list items li: ({ node, children, ...props }) => { const text = React.Children.toArray(children) .map((child) => { if (typeof child === "string") return child // @ts-expect-error - children props vary if (child?.props?.children) return child.props.children return "" }) .join("") if (containsMath(text)) { return (
  • ) } return (
  • {children}
  • ) }, ul: ({ ordered, ...props }) => createMarkdownElement(ordered ? "ol" : "ul", { className: "ml-6 list-disc text-secondary font-sans text-base font-normal", ...props, }), ol: ({ ordered, ...props }) => createMarkdownElement(ordered ? "ol" : "ul", { className: "list-decimal text-secondary font-sans text-base font-normal mt-3", ...props, }), table: Table, tr: TableRow, thead: TableHead, tbody: TableBody, "table-row-card": ({ node, children }: { node: any; children: string }) => { try { const content = JSON.parse(children) return } catch (error) { console.error("Error parsing table-row-card content:", error) return
    Error rendering table row card
    } }, accordion: ({ node, children }: { node: any; children: string }) => { try { const content = JSON.parse(children) return ( ) } catch (error) { console.error("Error parsing accordion content:", error) return
    Error rendering accordion
    } }, td: (props) => { const { node, children, ...rest } = props // Convert children to text to check for math const text = React.Children.toArray(children) .map((child) => { if (typeof child === "string") return child // @ts-expect-error - children props vary if (child?.props?.children) return child.props.children return "" }) .join("") // Handle line breaks in table cells by replacing
    with actual line breaks const hasBrTags = typeof text === "string" && text.includes("
    ") // Check if there's HTML content with style attribute in the cell const hasHtmlContent = typeof text === "string" && (text.includes("
    ) } if (hasHtmlContent) { const processedText = hasBrTags ? text.replace(/
    /g, "
    ") : text return ( ) } return ( {children} ) }, th: (props) => { const { node, children, ...rest } = props if (!children || (Array.isArray(children) && children.length === 0)) { return null } // Convert children to text to check for math const text = React.Children.toArray(children) .map((child) => { if (typeof child === "string") return child // @ts-expect-error - children props vary if (child?.props?.children) return child.props.children return "" }) .join("") // Handle line breaks in table headers by replacing
    with actual line breaks const hasBrTags = typeof text === "string" && text.includes("
    ") // Check if there's HTML content with style attribute in the cell const hasHtmlContent = typeof text === "string" && (text.includes("
    ) } if (hasHtmlContent) { const processedText = hasBrTags ? text.replace(/
    /g, "
    ") : text return ( ) } return ( {children} ) }, img: ({ ...props }) => createMarkdownElement("img", { className: "w-auto w-auto mx-auto rounded-lg object-cover", ...props, }), "footnote-ref": ({ identifier, label }) => { const handleClick = useCallback( (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() const target = document.getElementById(`fn-${identifier}`) if (target) { scrollToElementWithOffset(target) } }, [identifier] ) React.useEffect(() => { console.log("Footnote ref mounted:", identifier) }, [identifier]) return ( ) }, "footnote-definition": ({ identifier, label, children }) => { const handleBackClick = useCallback( (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() const target = document.getElementById(`fnref-${identifier}`) if (target) { scrollToElementWithOffset(target) } }, [identifier] ) React.useEffect(() => { console.log("Footnote definition mounted:", identifier) }, [identifier]) return (
    [{label}]
    {children}
    ) }, footnotes: ({ children }) => { return (

    Footnotes

    {children}
    ) }, }) interface MarkdownProps { children: string components?: Components // components overrides the default components darkMode?: boolean } export const Markdown = ({ children, components, darkMode = false, }: MarkdownProps) => { const [content, setContent] = React.useState([]) React.useEffect(() => { if (!children) { setContent([]) return } try { const blockMathRegex = /\$\$([\s\S]*?)\$\$/g const blockParts = children.split(blockMathRegex) const mathComponents = { ...REACT_MARKDOWN_CONFIG(darkMode), ...components, } const rehypePlugins = [rehypeRaw as any, rehypeProcessBrTags as any] if (blockParts.length === 1) { setContent([ {children} , ]) return } const parts: React.ReactNode[] = [] blockParts.forEach((part, index) => { if (index % 2 === 0) { if (part.trim()) { parts.push( {part} ) } } else { parts.push() } }) setContent(parts) } catch (error) { console.error("Error processing markdown with math:", error) setContent([ {children} , ]) } }, [children, darkMode, components]) return <>{content} }