import fs from 'fs' import path from 'path' import matter from 'gray-matter' import { remark } from 'remark' import remarkParse from 'remark-parse' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import remarkRehype from 'remark-rehype' import rehypeHighlight from 'rehype-highlight' import rehypeKatex from 'rehype-katex' import rehypeReact from 'rehype-react' import { cloneElement, isValidElement, type ReactElement, Fragment, type ReactNode } from 'react' import { jsx, jsxs } from 'react/jsx-runtime' import { rehypeCodeMeta } from '@/lib/rehype-code-meta' import { CodeWrapper } from '@/components/content/CodeWrapper' import { ImageWrapper } from '@/components/content/ImageWrapper' import { ExternalLink } from '@/components/ui/ExternalLink' import readingTime from 'reading-time' import { rehypeUnwrapImages } from '@/lib/rehype-unwrap-images' import { rehypeMermaidSsg } from '@/lib/rehype-mermaid-ssg' import { getTextContent, stripLeadingMatch } from '@/lib/react-node-text' import { BlockquoteWrapper } from '@/components/content/BlockquoteWrapper' import type { NoteType } from '@/types/Note.type' import { rehypeMark } from '@/lib/rehype-mark' import { rehypeDefinition } from '@/lib/rehype-definition' import { slugify } from '@/utils/slug' import { PostMeta } from '@/types/Post.type' import Link from 'next/link' import { ExternalLinkIcon } from 'lucide-react' const postsDirectory = path.join(process.cwd(), 'content/posts') export function getAllPostSlugs(): string[] { return fs .readdirSync(postsDirectory) .filter((f) => f.endsWith('.md')) .map((f) => f.replace(/\.md$/, '')) } export function getAllPostsMeta(): PostMeta[] { return getAllPostSlugs() .map((slug) => { const fullPath = path.join(postsDirectory, `${slug}.md`) const { data, content } = matter(fs.readFileSync(fullPath, 'utf8')) const readingTimeText = readingTime(content).text return Object.assign({ slug }, { ...data, readingTimeText, } as Omit) }) .toSorted((a, b) => (a.date < b.date ? 1 : -1)) } export function getNextAndPreviousPosts(slug: string): { next: PostMeta | null previous: PostMeta | null } { const posts = getAllPostsMeta() const index = posts.findIndex((post) => post.slug === slug) const next = index < posts.length - 1 ? posts[index + 1] : null const previous = index > 0 ? posts[index - 1] : null return { next, previous } } export async function getPostBySlug(slug: string) { const fullPath = path.join(postsDirectory, `${slug}.md`) const { data, content } = matter(fs.readFileSync(fullPath, 'utf8')) let processor = remark().use(remarkParse).use(remarkGfm) if (data.math) { processor = processor.use(remarkMath) } processor = processor .use(remarkRehype, { allowDangerousHtml: true }) .use(rehypeHighlight) .use(rehypeCodeMeta) .use(rehypeMark) .use(rehypeDefinition) if (data.math) { processor = processor.use(rehypeKatex, { output: 'mathml' }) } if (data.mermaid) { processor = processor.use(rehypeMermaidSsg) } processor = processor.use(rehypeUnwrapImages).use(rehypeReact, { Fragment, jsx, jsxs, components: { pre: ({ children, ...props }: { children: ReactNode; [key: string]: unknown }) => { const language = typeof props['data-language'] === 'string' ? props['data-language'] : undefined return {children} }, img: ({ src, alt }: { src: string; alt: string }) => , a: ({ href, children }: { href: string; children: ReactNode }) => href?.startsWith('http') ? ( {children} ) : ( {children} ), blockquote: ({ children }: { children: ReactNode }) => { const textContent = getTextContent(children).trimStart() const type = textContent.match(/^\[!(\w+)\]/)?.[1]?.toLowerCase() as NoteType | undefined return ( {stripLeadingMatch(children, /^\[!(\w+)\]/)} ) }, }, }) const file = await processor.process(content) const rawContentNode = file.result as ReactNode const readingTimeText = readingTime(content).text const tableOfContents: { text: string; id: string; level: number }[] = [] const seenIds = new Map() function withHeadingIds(node: ReactNode): ReactNode { if (!isValidElement(node)) return node const element = node as ReactElement<{ children?: ReactNode; id?: string }> if (element.type === 'h2' || element.type === 'h3') { const text = getTextContent(element.props.children ?? []) let id = slugify(text) const seenCount = seenIds.get(id) ?? 0 seenIds.set(id, seenCount + 1) if (seenCount > 0) id = `${id}-${seenCount}` const level = element.type === 'h2' ? 2 : 3 tableOfContents.push({ text, id, level }) return cloneElement(element, { id }) } return node } const rawChildren = isValidElement(rawContentNode) ? (rawContentNode.props as { children?: ReactNode }).children : rawContentNode const childrenArray = Array.isArray(rawChildren) ? rawChildren : [rawChildren] const processedChildren = childrenArray.map(withHeadingIds) const processedContent = isValidElement(rawContentNode) ? cloneElement(rawContentNode, {}, processedChildren) : rawContentNode return { meta: { ...(data as Omit), readingTimeText }, content: processedContent, tableOfContents, } }