Refactor anchor link handling and enhance scrolling functionality. Update markdown processing to support GitHub-style anchor links and improve user experience with smooth scrolling to headings. Add debugging utilities for anchor links in development.
This commit is contained in:
@@ -36,11 +36,89 @@ function generateId(text: string): string {
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
// Enhanced slugification function that matches GitHub-style anchor links
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens
|
||||
.replace(/[\s_-]+/g, '-') // Replace spaces, underscores, and multiple hyphens with single hyphen
|
||||
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
||||
}
|
||||
|
||||
// Function to process anchor links in markdown content
|
||||
function processAnchorLinks(content: string): string {
|
||||
// Find all markdown links that point to anchors (e.g., [text](#anchor))
|
||||
return content.replace(/\[([^\]]+)\]\(#([^)]+)\)/g, (match, linkText, anchor) => {
|
||||
// Only slugify if the anchor doesn't already look like a slug
|
||||
// This prevents double-processing of already-correct anchor links
|
||||
const isAlreadySlugified = /^[a-z0-9-]+$/.test(anchor);
|
||||
const slugifiedAnchor = isAlreadySlugified ? anchor : slugify(anchor);
|
||||
return `[${linkText}](#${slugifiedAnchor})`;
|
||||
});
|
||||
}
|
||||
|
||||
// Utility function to debug anchor links (for development)
|
||||
export function debugAnchorLinks(content: string): void {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
console.log('=== Anchor Link Debug Info ===');
|
||||
|
||||
// Extract all headings and their IDs
|
||||
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
|
||||
const headings: Array<{ level: number; text: string; id: string }> = [];
|
||||
|
||||
let match;
|
||||
while ((match = headingRegex.exec(content)) !== null) {
|
||||
const level = match[1].length;
|
||||
const text = match[2].trim();
|
||||
const id = slugify(text);
|
||||
headings.push({ level, text, id });
|
||||
}
|
||||
|
||||
console.log('Generated heading IDs:');
|
||||
headings.forEach(({ level, text, id }) => {
|
||||
console.log(` H${level}: "${text}" -> id="${id}"`);
|
||||
});
|
||||
|
||||
// Extract all anchor links
|
||||
const anchorLinkRegex = /\[([^\]]+)\]\(#([^)]+)\)/g;
|
||||
const anchorLinks: Array<{ linkText: string; originalAnchor: string; slugifiedAnchor: string }> = [];
|
||||
|
||||
while ((match = anchorLinkRegex.exec(content)) !== null) {
|
||||
const linkText = match[1];
|
||||
const originalAnchor = match[2];
|
||||
const slugifiedAnchor = slugify(originalAnchor);
|
||||
anchorLinks.push({ linkText, originalAnchor, slugifiedAnchor });
|
||||
}
|
||||
|
||||
console.log('Anchor links found:');
|
||||
anchorLinks.forEach(({ linkText, originalAnchor, slugifiedAnchor }) => {
|
||||
const headingExists = headings.some(h => h.id === slugifiedAnchor);
|
||||
const status = headingExists ? '✅' : '❌';
|
||||
console.log(` ${status} [${linkText}](#${originalAnchor}) -> [${linkText}](#${slugifiedAnchor})`);
|
||||
});
|
||||
|
||||
// Show missing headings
|
||||
const missingAnchors = anchorLinks.filter(({ slugifiedAnchor }) =>
|
||||
!headings.some(h => h.id === slugifiedAnchor)
|
||||
);
|
||||
|
||||
if (missingAnchors.length > 0) {
|
||||
console.warn('Missing headings for these anchor links:');
|
||||
missingAnchors.forEach(({ linkText, originalAnchor, slugifiedAnchor }) => {
|
||||
console.warn(` - [${linkText}](#${originalAnchor}) -> id="${slugifiedAnchor}"`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('=== End Debug Info ===');
|
||||
}
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
// Custom heading renderer to add IDs
|
||||
renderer.heading = (text, level) => {
|
||||
const id = generateId(text);
|
||||
const id = slugify(text);
|
||||
return `<h${level} id="${id}">${text}</h${level}>`;
|
||||
};
|
||||
|
||||
@@ -68,7 +146,12 @@ export async function getPostBySlug(slug: string): Promise<Post> {
|
||||
|
||||
let processedContent = '';
|
||||
try {
|
||||
const rawHtml = marked.parse(content);
|
||||
// Debug anchor links in development
|
||||
debugAnchorLinks(content);
|
||||
|
||||
// Process anchor links before parsing markdown
|
||||
const processedMarkdown = processAnchorLinks(content);
|
||||
const rawHtml = marked.parse(processedMarkdown);
|
||||
const window = new JSDOM('').window;
|
||||
const purify = DOMPurify(window);
|
||||
processedContent = purify.sanitize(rawHtml as string, {
|
||||
|
||||
Reference in New Issue
Block a user