diff --git a/components/search/search-modal.tsx b/components/search/search-modal.tsx index 8accb25..e10f8ea 100644 --- a/components/search/search-modal.tsx +++ b/components/search/search-modal.tsx @@ -92,7 +92,7 @@ function NoResults() { const LoadingIndicator = () => { return ( -
+
) } @@ -319,14 +319,14 @@ export const SearchModal = ({ open, setOpen }: SearchModalProps) => { } return ( - +
diff --git a/components/ui/markdown.tsx b/components/ui/markdown.tsx index eb6bb69..bd3f3c9 100644 --- a/components/ui/markdown.tsx +++ b/components/ui/markdown.tsx @@ -3,8 +3,7 @@ import React from "react" import ReactMarkdown, { Components } from "react-markdown" import remarkGfm from "remark-gfm" -import remarkMath from "remark-math" -import rehypeKatex from "rehype-katex" +import katex from "katex" import "katex/dist/katex.min.css" const generateSectionId = (text: string) => { @@ -107,6 +106,144 @@ const remarkCustomNewlines = () => { } } +// Custom math components +const KaTeXBlock = ({ math }: { math: string }) => { + const mathRef = React.useRef(null) + + React.useEffect(() => { + if (mathRef.current) { + try { + katex.render(math.trim(), 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 { + katex.render(math.trim(), mathRef.current, { + displayMode: false, + throwOnError: false, + }) + } catch (error) { + console.error("KaTeX inline render error:", error) + if (mathRef.current) { + mathRef.current.textContent = math + } + } + } + }, [math]) + + return +} + +// Define a simple regex to extract math content from our markdown +const extractMathBlocks = (content: string) => { + const blockMathRegex = /\$\$([\s\S]*?)\$\$/g + const inlineMathRegex = /\$(.*?)\$/g + + const mathBlocks: { + start: number + end: number + content: string + isBlock: boolean + }[] = [] + + let match + while ((match = blockMathRegex.exec(content)) !== null) { + mathBlocks.push({ + start: match.index, + end: match.index + match[0].length, + content: match[1], + isBlock: true, + }) + } + + while ((match = inlineMathRegex.exec(content)) !== null) { + // Make sure this isn't already part of a block math section + const isInsideBlockMath = mathBlocks.some( + (block) => match!.index > block.start && match!.index < block.end + ) + + if (!isInsideBlockMath) { + mathBlocks.push({ + start: match.index, + end: match.index + match[0].length, + content: match[1], + isBlock: false, + }) + } + } + + mathBlocks.sort((a, b) => a.start - b.start) + + return mathBlocks +} + +const MathText = ({ text }: { text: string }) => { + const parts: React.ReactNode[] = [] + let lastIndex = 0 + + if (/^\$(.*?)\$$/m.test(text.trim())) { + const mathContent = text.trim().slice(1, -1) + return + } + + try { + // Regular expression to match dollar signs that aren't escaped with a backslash + const inlineMathRegex = /(? lastIndex) { + parts.push(text.slice(lastIndex, match.index)) + } + + const mathContent = match[1].trim() + parts.push( + + ) + + lastIndex = match.index + match[0].length + } + + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)) + } + + if (parts.length === 0 && text.includes("$")) { + return <>{text} + } + + return <>{parts} + } catch (error) { + console.error("Error processing inline math:", error) + return <>{text} + } +} + +// Helper to check if text contains unescaped math delimiters +const containsMath = (text: string): boolean => { + if (!text.includes("$")) return false + + const inlineMathRegex = /(? ({ a: ({ ...props }) => @@ -145,11 +282,61 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): Components => ({ className: "text-neutral-800 text-md font-bold", ...props, }), - p: ({ ...props }) => - createMarkdownElement("p", { - className: `${darkMode ? "text-white" : "text-tuatara-700 "} font-sans text-base font-normal`, + p: ({ 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 (text.includes("$")) { + return createMarkdownElement("p", { + className: `${darkMode ? "text-white" : "text-tuatara-700"} font-sans text-base font-normal`, + children: , + ...props, + }) + } + + return createMarkdownElement("p", { + className: `${darkMode ? "text-white" : "text-tuatara-700"} font-sans text-base font-normal`, + children, ...props, - }), + }) + }, + // 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: @@ -166,18 +353,63 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): Components => ({ tr: TableRow, thead: TableHead, td: (props) => { - const { node, ...rest } = props - return + 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("") + + // Check if there's math content + if (containsMath(text)) { + return ( + + + + ) + } + + return ( + + {children} + + ) }, th: (props) => { - const { node, ...rest } = props - if ( - !props.children || - (Array.isArray(props.children) && props.children.length === 0) - ) { + const { node, children, ...rest } = props + if (!children || (Array.isArray(children) && children.length === 0)) { return null } - return + + // 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("") + + // Check if there's math content + if (containsMath(text)) { + return ( + + + + ) + } + + return ( + + {children} + + ) }, pre: ({ ...props }) => createMarkdownElement("pre", { @@ -202,17 +434,76 @@ export const Markdown = ({ components, darkMode = false, }: MarkdownProps) => { - return ( - ([]) + + React.useEffect(() => { + if (!children) { + setContent([]) + return + } + + try { + const blockMathRegex = /\$\$([\s\S]*?)\$\$/g + const blockParts = children.split(blockMathRegex) + + const mathComponents = { ...REACT_MARKDOWN_CONFIG(darkMode), ...components, - }} - remarkPlugins={[remarkGfm, remarkMath, remarkCustomNewlines]} - rehypePlugins={[rehypeKatex as any]} - > - {children} - - ) + } + + 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} } diff --git a/styles/globals.css b/styles/globals.css index b2809b4..fb341a5 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -172,11 +172,10 @@ @apply block; } -span.katex { - text-align: center; + +.katex-display { width: 100%; - display: block; - padding: 10px 0; + float: left; } /*Accordion hover state content*/ @media screen and (max-width: 768px) {