eolas-app/src/components/EntryBody.tsx

122 lines
4.6 KiB
TypeScript
Raw Normal View History

import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import CodeBlock from "@/components/CodeBlock"
import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex"
import "katex/dist/katex.min.css"
2025-09-26 14:33:07 +01:00
import BodyLink from "./BodyLink"
2025-12-11 19:17:37 +00:00
import EntryLoadingSkeleton from "./EntryLoadingSkeleton"
import { useSearchParams } from "react-router"
2025-08-14 16:31:25 +01:00
const ImagePreprocessor = (src) => {
2025-09-26 14:33:07 +01:00
const filename = src.src.split("/").pop()
const s3RootUrl = "https://eolas.s3.systemsobscure.net/"
return <img src={s3RootUrl + filename} />
2025-08-14 16:31:25 +01:00
}
2025-12-11 19:17:37 +00:00
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
const highlighter = (children, highlight) => {
if (!highlight || typeof children !== "string") return children
const words = highlight.trim().split(/\s+/)
const pattern = words.length > 1 ? escapeRegex(highlight) : escapeRegex(words[0])
const regex = new RegExp(`\\b(${pattern})\\b`, "gi")
const parts = children.split(regex)
return parts.map((part, i) =>
regex.test(part) ? (
<mark key={i} className="dark:bg-[#4c1d95] dark:text-white bg-[#ddd6fe]">
{part}
</mark>
) : (
part
),
)
}
export default function EntryBody({ body, isLoading }) {
2025-12-11 19:17:37 +00:00
const [searchParams] = useSearchParams()
const highlight = searchParams.get("highlight")
2025-09-26 14:33:07 +01:00
if (isLoading) {
return <EntryLoadingSkeleton />
2025-12-11 19:17:37 +00:00
}
return (
<div className="max-w-2xl p-4 lg:p-6">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
h1: () => null,
h2: ({ children }) => (
<h2 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">
{highlighter(children, highlight)}
</h2>
),
h3: ({ children }) => (
<h3 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">
{highlighter(children, highlight)}
</h3>
),
h4: ({ children }) => (
<h4 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">
{highlighter(children, highlight)}
</h4>
),
p: ({ children }) => (
<p className="leading-[1.5] mb-4 not-first:mt-4">
{highlighter(children, highlight)}
</p>
),
ul: ({ children }) => <ul className="list-disc ml-10 mb-4 space-y-1">{children}</ul>,
ol: ({ children }) => (
<ol className="list-decimal ml-10 mb-4 space-y-1">{children}</ol>
),
li: ({ children }) => (
<li className="list-disc ml-10 mb-4 space-y-1">
{highlighter(children, highlight)}
</li>
),
table: ({ children }) => <table className="w-full mb-4 text-sm">{children}</table>,
tr: ({ children }) => (
<tr className="even:bg-muted m-0 border-t p-0">
{highlighter(children, highlight)}
</tr>
),
th: ({ children }) => (
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
{highlighter(children, highlight)}
</th>
),
td: ({ children }) => (
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
{highlighter(children, highlight)}
</td>
),
blockquote: ({ children }) => (
<blockquote className="mt-4 border-l-2 pl-6 text-muted-foreground">
{highlighter(children, highlight)}
</blockquote>
),
pre: ({ children }) => {
const child = children.props
return <CodeBlock className={child.className}>{child.children}</CodeBlock>
},
code: ({ children }) => (
<code className="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
{children}
</code>
),
img: ({ src }) => <ImagePreprocessor src={src} />,
a: ({ href, children }) => {
return <BodyLink link={href} children={children} />
},
}}
>
{body}
</ReactMarkdown>
</div>
)
}