Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 84 additions & 54 deletions components/ui/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,78 @@ const MathText = ({ text }: { text: string }) => {
}
}

const getMarkdownElementTag = (element: React.ReactElement): string | null => {
if (typeof element.type === "string") {
return element.type
}

const node = (element.props as { node?: { tagName?: unknown } })?.node
return typeof node?.tagName === "string" ? node.tagName : null
}

const shouldSkipMathChildren = (element: React.ReactElement): boolean => {
const tagName = getMarkdownElementTag(element)
return tagName === "code" || tagName === "pre"
}

const getTextContent = (children: React.ReactNode): string => {
return React.Children.toArray(children)
.map((child) => {
if (typeof child === "string" || typeof child === "number") {
return String(child)
}

if (React.isValidElement(child)) {
return getTextContent(
(child.props as { children?: React.ReactNode }).children
)
}

return ""
})
.join("")
}

const containsMathInChildren = (children: React.ReactNode): boolean => {
return React.Children.toArray(children).some((child) => {
if (typeof child === "string") {
return containsMath(child)
}

if (React.isValidElement(child) && !shouldSkipMathChildren(child)) {
return containsMathInChildren(
(child.props as { children?: React.ReactNode }).children
)
}

return false
})
}

const renderMathInChildren = (children: React.ReactNode): React.ReactNode => {
return React.Children.map(children, (child) => {
if (typeof child === "string") {
return containsMath(child) ? <MathText text={child} /> : child
}

if (React.isValidElement(child) && !shouldSkipMathChildren(child)) {
const childProps = child.props as { children?: React.ReactNode }

if (childProps.children === undefined) {
return child
}

return React.cloneElement(
child as React.ReactElement<{ children?: React.ReactNode }>,
undefined,
renderMathInChildren(childProps.children)
)
}

return child
})
}

const rehypeProcessBrTags = () => {
return (tree: any) => {
const visit = (node: any) => {
Expand Down Expand Up @@ -535,18 +607,7 @@ const HeadingLink = ({
if (typeof children === "string") {
return generateSectionId(children)
}
const text = React.Children.toArray(children)
.map((child) => {
if (typeof child === "string") return child
if (
React.isValidElement(child) &&
typeof child.props?.children === "string"
) {
return child.props.children
}
return ""
})
.join("")
const text = getTextContent(children)
return generateSectionId(text)
}, [children])

Expand Down Expand Up @@ -661,15 +722,7 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
}

// 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("")
const text = getTextContent(children)

let isMathOnly = false
if (text.trim().startsWith("$$") && text.trim().endsWith("$$")) {
Expand All @@ -679,14 +732,14 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
}
}

if (containsMath(text)) {
if (containsMathInChildren(children)) {
return (
<p
className={`text-tuatara-600 dark:text-tuatara-200 font-sans text-lg font-normal ${
isMathOnly ? "math-only" : ""
}`}
>
<MathText text={text} />
{renderMathInChildren(children)}
</p>
)
}
Expand All @@ -699,22 +752,13 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
},
// 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)) {
if (containsMathInChildren(children)) {
return (
<li
className="text-tuatara-500 font-sans text-base lg:text-lg font-normal dark:text-tuatara-100"
{...props}
>
<MathText text={text} />
{renderMathInChildren(children)}
</li>
)
}
Expand Down Expand Up @@ -775,14 +819,7 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
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("")
const text = getTextContent(children)

// Handle line breaks in table cells by replacing <br> with actual line breaks
const hasBrTags = typeof text === "string" && text.includes("<br>")
Expand All @@ -792,10 +829,10 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
typeof text === "string" && (text.includes("<div style=") || hasBrTags)

// Check if there's math content
if (containsMath(text)) {
if (containsMathInChildren(children)) {
return (
<td className="p-4 text-left" {...rest}>
<MathText text={text} />
{renderMathInChildren(children)}
</td>
)
}
Expand Down Expand Up @@ -824,14 +861,7 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
}

// 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("")
const text = getTextContent(children)

// Handle line breaks in table headers by replacing <br> with actual line breaks
const hasBrTags = typeof text === "string" && text.includes("<br>")
Expand All @@ -840,10 +870,10 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({
const hasHtmlContent =
typeof text === "string" && (text.includes("<div style=") || hasBrTags)

if (containsMath(text)) {
if (containsMathInChildren(children)) {
return (
<th className="p-4 text-left font-medium" {...rest}>
<MathText text={text} />
{renderMathInChildren(children)}
</th>
)
}
Expand Down
24 changes: 24 additions & 0 deletions tests/components/markdown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Markdown } from "@/components/ui/markdown"
import { render, screen, waitFor } from "@testing-library/react"
import { describe, expect, it } from "vitest"

describe("Markdown", () => {
it("preserves inline code in footnotes that also contain math", async () => {
const { container } = render(
<Markdown>
{
"See the schedule score.[^score]\n\n[^score]: For a schedule $s$, the scorer computes `score(s)`."
}
</Markdown>
)

await waitFor(() => {
expect(container).toHaveTextContent("For a schedule")
})

expect(container.textContent).not.toContain("[object Object]")

const code = screen.getByText("score(s)")
expect(code.tagName.toLowerCase()).toBe("code")
})
})
Loading