"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypeHighlight from "rehype-highlight"; import "highlight.js/styles/atom-one-dark.min.css"; import styles from "./lesson-markdown.module.css"; type TocItem = { level: number; text: string; id: string }; function extractToc(content: string): TocItem[] { const items: TocItem[] = []; const lines = content.split("\n"); let idCounter = 0; for (const line of lines) { const h1 = line.match(/^# (.+)$/); const h2 = line.match(/^## (.+)$/); if (h1) { items.push({ level: 1, text: h1[1].replace(/#{1,6}\s/g, "").trim(), id: `h-${idCounter++}` }); } else if (h2) { items.push({ level: 2, text: h2[1].replace(/#{1,6}\s/g, "").trim(), id: `h-${idCounter++}` }); } } return items; } function slugify(text: string): string { return text .replace(/[^\p{L}\p{N}\s-]/gu, "") .replace(/\s+/g, "-") .toLowerCase(); } export function LessonMarkdown({ content }: { content: string }) { const [readProgress, setReadProgress] = useState(0); const [showBackTop, setShowBackTop] = useState(false); const [showToc, setShowToc] = useState(false); const contentRef = useRef(null); const toc = extractToc(content); useEffect(() => { let ticking = false; const onScroll = () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { const scrollTop = window.scrollY || document.documentElement.scrollTop; const docHeight = document.documentElement.scrollHeight - window.innerHeight; setReadProgress(docHeight > 0 ? Math.min(100, (scrollTop / docHeight) * 100) : 0); setShowBackTop(scrollTop > 400); ticking = false; }); }; window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); const handleTocJump = useCallback((index: number) => { setShowToc(false); const root = contentRef.current; if (!root) return; const headings = root.querySelectorAll("h1, h2"); const el = headings[index] as HTMLElement; if (el) { el.scrollIntoView({ behavior: "smooth", block: "start" }); } }, []); return ( <>
{ const text = String(children); const id = slugify(text); return (

{children}

); }, h2: ({ children }) => { const text = String(children); const id = slugify(text); return (

{children}

); }, h3: ({ children }) =>

{children}

, h4: ({ children }) =>

{children}

, h5: ({ children }) =>
{children}
, h6: ({ children }) =>
{children}
, p: ({ children }) =>

{children}

, ul: ({ children }) =>
    {children}
, ol: ({ children }) =>
    {children}
, li: ({ children, className }) => (
  • {children}
  • ), pre: ({ children }) => (
    {children}
    ), code: ({ className, children }) => ( {children} ), blockquote: ({ children }) =>
    {children}
    , a: ({ href, children }) => ( {children} ), img: ({ src, alt }) => ( // eslint-disable-next-line @next/next/no-img-element {alt ), table: ({ children }) => (
    {children}
    ), th: ({ children }) => {children}, td: ({ children }) => {children}, hr: () =>
    , strong: ({ children }) => {children}, em: ({ children }) => {children}, }} > {content}
    {/* 目录抽屉 */}
    setShowToc(false)} aria-hidden="true" /> {showBackTop && ( <> {toc.length > 0 && ( )} )} ); }