Refactor TOC to use rehype-slug
- Rename package to `goodgo-docs` - Add `rehype-slug` for automatic heading ID generation - Simplify TableOfContents component by removing manual ID logic
This commit is contained in:
@@ -21,51 +21,19 @@ export default function TableOfContents({ locale }: TableOfContentsProps) {
|
||||
|
||||
// Extract headings from content
|
||||
useEffect(() => {
|
||||
|
||||
const generateSlug = (text: string): string => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim();
|
||||
};
|
||||
|
||||
const extractHeadings = (): HeadingItem[] => {
|
||||
const content = document.querySelector('[data-docs-content]');
|
||||
if (!content) return [];
|
||||
|
||||
const headingElements = content.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
const headingItems: HeadingItem[] = [];
|
||||
const usedIds = new Set<string>();
|
||||
|
||||
headingElements.forEach((heading) => {
|
||||
const text = heading.textContent?.trim() || '';
|
||||
let id = heading.id || heading.getAttribute('id') || '';
|
||||
|
||||
// If no ID exists, generate one from text
|
||||
if (!id && text) {
|
||||
id = generateSlug(text);
|
||||
}
|
||||
|
||||
// Deduplicate ID
|
||||
if (id) {
|
||||
let uniqueId = id;
|
||||
let counter = 1;
|
||||
while (usedIds.has(uniqueId)) {
|
||||
uniqueId = `${id}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
id = uniqueId;
|
||||
|
||||
// Update the actual element ID so clicking TOC links works correctly
|
||||
heading.id = id;
|
||||
usedIds.add(id);
|
||||
}
|
||||
|
||||
const level = parseInt(heading.tagName.charAt(1));
|
||||
const id = heading.id;
|
||||
|
||||
if (id && text) {
|
||||
const level = parseInt(heading.tagName.charAt(1));
|
||||
headingItems.push({ id, text, level });
|
||||
}
|
||||
});
|
||||
@@ -74,21 +42,19 @@ export default function TableOfContents({ locale }: TableOfContentsProps) {
|
||||
};
|
||||
|
||||
// Function to update headings
|
||||
const updateHeadings = (): boolean => {
|
||||
const updateHeadings = () => {
|
||||
const headingItems = extractHeadings();
|
||||
setHeadings(headingItems);
|
||||
// Reset active ID if we have no headings (e.g. page transition)
|
||||
if (headingItems.length === 0) {
|
||||
setActiveId('');
|
||||
}
|
||||
return headingItems.length > 0;
|
||||
};
|
||||
|
||||
// Set up MutationObserver to watch for content changes
|
||||
const content = document.querySelector('[data-docs-content]');
|
||||
let observer: MutationObserver | null = null;
|
||||
let timers: NodeJS.Timeout[] = [];
|
||||
let foundHeadings = false;
|
||||
|
||||
const cleanup = () => {
|
||||
if (observer) {
|
||||
@@ -99,62 +65,31 @@ export default function TableOfContents({ locale }: TableOfContentsProps) {
|
||||
timers = [];
|
||||
};
|
||||
|
||||
// Function to try extracting with cleanup if successful
|
||||
const tryExtractAndCleanup = () => {
|
||||
if (updateHeadings() && !foundHeadings) {
|
||||
foundHeadings = true;
|
||||
// Keep observer running for dynamic updates, but stop timers
|
||||
timers.forEach(t => clearTimeout(t));
|
||||
timers = [];
|
||||
}
|
||||
};
|
||||
|
||||
// Try after a tiny delay to avoid synchronous setState in effect body (lint rule)
|
||||
const initialTimer = setTimeout(() => {
|
||||
tryExtractAndCleanup();
|
||||
}, 0);
|
||||
// Try extracting immediately
|
||||
const initialTimer = setTimeout(updateHeadings, 0);
|
||||
timers.push(initialTimer);
|
||||
|
||||
if (content) {
|
||||
// Watch for changes in the content
|
||||
// Watch for changes in the content structure (loading, hydration)
|
||||
observer = new MutationObserver(() => {
|
||||
tryExtractAndCleanup();
|
||||
updateHeadings();
|
||||
});
|
||||
|
||||
observer.observe(content, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['id'],
|
||||
// No longer watching attributes since we don't modify them
|
||||
});
|
||||
}
|
||||
|
||||
// Also try periodically in case content loads later (especially for dynamic MDX imports)
|
||||
const tryMultipleTimes = [100, 300, 500, 1000, 2000, 3000];
|
||||
// Try periodically for dynamic content
|
||||
const tryMultipleTimes = [100, 300, 500, 1000];
|
||||
tryMultipleTimes.forEach((delay) => {
|
||||
const timer = setTimeout(() => {
|
||||
if (!foundHeadings) {
|
||||
tryExtractAndCleanup();
|
||||
}
|
||||
}, delay);
|
||||
const timer = setTimeout(updateHeadings, delay);
|
||||
timers.push(timer);
|
||||
});
|
||||
|
||||
// Also listen for route changes (Next.js navigation)
|
||||
const handleRouteChange = () => {
|
||||
foundHeadings = false;
|
||||
// Reset and try again after a short delay
|
||||
setTimeout(() => {
|
||||
tryExtractAndCleanup();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handleRouteChange);
|
||||
|
||||
return () => {
|
||||
cleanup();
|
||||
window.removeEventListener('popstate', handleRouteChange);
|
||||
};
|
||||
return cleanup;
|
||||
}, [pathname]); // Re-run when pathname changes
|
||||
|
||||
// Set up intersection observer for active heading
|
||||
|
||||
Reference in New Issue
Block a user