"use client";
import { useEffect, useState, useCallback, useRef } from "react";
import { marked } from "marked";
import styles from "./lesson-markdown.module.css";
type TocItem = { level: number; text: string; id: string };
function slugify(text: string): string {
return text
.replace(/[^\p{L}\p{N}\s-]/gu, "")
.replace(/\s+/g, "-")
.toLowerCase();
}
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 markdownToHtml(content: string): string {
const raw = marked.parse(content, { breaks: true, gfm: true }) as string;
let html = raw
.replace(/');
html = html.replace(/
\s*]*)>/g, ``);
return html;
}
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);
const html = markdownToHtml(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 (
<>
setShowToc(false)}
aria-hidden="true"
/>
{showBackTop && (
<>
{toc.length > 0 && (
)}
>
)}
>
);
}