TextBodySection.tsx44 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import React from 'react' |
| 4 | import type { Components } from 'react-markdown' |
| 5 | import ReactMarkdown from 'react-markdown' |
| 6 | |
| 7 | import type { GoTextBodySection } from '../schemas' |
| 8 | import { slugify } from './TableOfContents' |
| 9 | |
| 10 | function getTextContent(node: React.ReactNode): string { |
| 11 | if (typeof node === 'string') return node |
| 12 | if (typeof node === 'number') return String(node) |
| 13 | if (Array.isArray(node)) return node.map(getTextContent).join('') |
| 14 | if (React.isValidElement(node) && node.props) { |
| 15 | return getTextContent((node.props as { children?: React.ReactNode }).children) |
| 16 | } |
| 17 | return '' |
| 18 | } |
| 19 | |
| 20 | const headingWithId = |
| 21 | (Tag: 'h1' | 'h2' | 'h3'): Components[typeof Tag] => |
| 22 | ({ children }) => { |
| 23 | const text = getTextContent(children) |
| 24 | const id = slugify(text) |
| 25 | return ( |
| 26 | <Tag id={id} className="scroll-mt-24"> |
| 27 | {children} |
| 28 | </Tag> |
| 29 | ) |
| 30 | } |
| 31 | |
| 32 | const components: Components = { |
| 33 | h1: headingWithId('h1'), |
| 34 | h2: headingWithId('h2'), |
| 35 | h3: headingWithId('h3'), |
| 36 | } |
| 37 | |
| 38 | export default function TextBodySection({ section }: { section: GoTextBodySection }) { |
| 39 | return ( |
| 40 | <article className="prose prose-headings:text-foreground prose-p:text-foreground-light prose-strong:text-foreground prose-li:text-foreground-light prose-a:text-brand-link prose-a:decoration-brand-link max-w-none"> |
| 41 | <ReactMarkdown components={components}>{section.content}</ReactMarkdown> |
| 42 | </article> |
| 43 | ) |
| 44 | } |