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:
101
posts/anchor-test.md
Normal file
101
posts/anchor-test.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
title: Anchor Link Test
|
||||||
|
date: '2025-01-20'
|
||||||
|
tags:
|
||||||
|
- test
|
||||||
|
- anchors
|
||||||
|
summary: Testing anchor link functionality
|
||||||
|
author: Test Author
|
||||||
|
---
|
||||||
|
|
||||||
|
# Anchor Link Test
|
||||||
|
|
||||||
|
This is a test page to verify that anchor links work correctly.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Basic Headings](#basic-headings)
|
||||||
|
- [Special Characters](#special-characters)
|
||||||
|
- [Numbers and Symbols](#numbers-and-symbols)
|
||||||
|
- [Long Headings](#long-headings)
|
||||||
|
- [Nested Sections](#nested-sections)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This section tests basic anchor linking functionality.
|
||||||
|
|
||||||
|
## Basic Headings
|
||||||
|
|
||||||
|
### Simple Heading
|
||||||
|
This is a simple heading with basic text.
|
||||||
|
|
||||||
|
### Another Heading
|
||||||
|
This is another heading to test multiple anchors.
|
||||||
|
|
||||||
|
## Special Characters
|
||||||
|
|
||||||
|
### Heading with Special Chars: @#$%^&*()
|
||||||
|
This heading contains special characters that should be properly slugified.
|
||||||
|
|
||||||
|
### Heading with Spaces and Dashes
|
||||||
|
This heading has spaces and should be converted to dashes.
|
||||||
|
|
||||||
|
### Heading_with_Underscores
|
||||||
|
This heading uses underscores instead of spaces.
|
||||||
|
|
||||||
|
## Numbers and Symbols
|
||||||
|
|
||||||
|
### Heading with Numbers 123
|
||||||
|
This heading includes numbers.
|
||||||
|
|
||||||
|
### Heading with Symbols !@#$%^&*()
|
||||||
|
This heading has various symbols.
|
||||||
|
|
||||||
|
## Long Headings
|
||||||
|
|
||||||
|
### This is a Very Long Heading That Should Still Work Properly Even When It Contains Many Words and Characters
|
||||||
|
This heading is intentionally long to test slugification with extended text.
|
||||||
|
|
||||||
|
## Nested Sections
|
||||||
|
|
||||||
|
### Level 3 Heading
|
||||||
|
This is a level 3 heading.
|
||||||
|
|
||||||
|
#### Level 4 Heading
|
||||||
|
This is a level 4 heading.
|
||||||
|
|
||||||
|
##### Level 5 Heading
|
||||||
|
This is a level 5 heading.
|
||||||
|
|
||||||
|
###### Level 6 Heading
|
||||||
|
This is a level 6 heading.
|
||||||
|
|
||||||
|
### Another Level 3
|
||||||
|
This is another level 3 heading.
|
||||||
|
|
||||||
|
## Test Links
|
||||||
|
|
||||||
|
You can test the anchor links by clicking on these:
|
||||||
|
|
||||||
|
- [Go to Overview](#overview)
|
||||||
|
- [Go to Basic Headings](#basic-headings)
|
||||||
|
- [Go to Special Characters](#special-characters)
|
||||||
|
- [Go to Numbers and Symbols](#numbers-and-symbols)
|
||||||
|
- [Go to Long Headings](#long-headings)
|
||||||
|
- [Go to Nested Sections](#nested-sections)
|
||||||
|
- [Go to Level 3 Heading](#level-3-heading)
|
||||||
|
- [Go to Level 4 Heading](#level-4-heading)
|
||||||
|
- [Go to Level 5 Heading](#level-5-heading)
|
||||||
|
- [Go to Level 6 Heading](#level-6-heading)
|
||||||
|
|
||||||
|
## Simple Test
|
||||||
|
|
||||||
|
### Test Heading
|
||||||
|
This is a simple test heading to verify anchor linking works.
|
||||||
|
|
||||||
|
- [Go to Test Heading](#test-heading)
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
If all the links above work correctly, the anchor linking system is functioning properly!
|
||||||
@@ -11,23 +11,16 @@ author: Rattatwinko's
|
|||||||
|
|
||||||
* [Overview](#overview)
|
* [Overview](#overview)
|
||||||
* [Philosophy](#philosophy)
|
* [Philosophy](#philosophy)
|
||||||
* [Inline HTML](#html)
|
* [Block Elements](#block-elements)
|
||||||
* [Automatic Escaping for Special Characters](#autoescape)
|
* [Paragraphs and Line Breaks](#paragraphs-and-line-breaks)
|
||||||
* [Block Elements](#block)
|
* [Headers](#headers)
|
||||||
* [Paragraphs and Line Breaks](#p)
|
* [Blockquotes](#blockquotes)
|
||||||
* [Headers](#header)
|
* [Lists](#lists)
|
||||||
* [Blockquotes](#blockquote)
|
* [Code Blocks](#code-blocks)
|
||||||
* [Lists](#list)
|
* [Span Elements](#span-elements)
|
||||||
* [Code Blocks](#precode)
|
* [Links](#links)
|
||||||
* [Horizontal Rules](#hr)
|
* [Emphasis](#emphasis)
|
||||||
* [Span Elements](#span)
|
|
||||||
* [Links](#link)
|
|
||||||
* [Emphasis](#em)
|
|
||||||
* [Code](#code)
|
* [Code](#code)
|
||||||
* [Images](#img)
|
|
||||||
* [Miscellaneous](#misc)
|
|
||||||
* [Backslash Escapes](#backslash)
|
|
||||||
* [Automatic Links](#autolink)
|
|
||||||
|
|
||||||
|
|
||||||
**Note:** This document is itself written using Markdown; you
|
**Note:** This document is itself written using Markdown; you
|
||||||
|
|||||||
@@ -57,6 +57,47 @@ html {
|
|||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Enhanced anchor link styles */
|
||||||
|
.prose a[href^="#"] {
|
||||||
|
color: #059669; /* Green color for anchor links */
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a[href^="#"]:hover {
|
||||||
|
color: #047857;
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add subtle visual indicator for headings that have anchor links */
|
||||||
|
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h1:hover::before,
|
||||||
|
.prose h2:hover::before,
|
||||||
|
.prose h3:hover::before,
|
||||||
|
.prose h4:hover::before,
|
||||||
|
.prose h5:hover::before,
|
||||||
|
.prose h6:hover::before {
|
||||||
|
content: "🔗";
|
||||||
|
position: absolute;
|
||||||
|
left: -1.5rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.8em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper spacing for anchor link indicators */
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
|
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
|
||||||
color: #111827;
|
color: #111827;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
@@ -35,124 +35,151 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!post) return;
|
if (!post) return;
|
||||||
|
|
||||||
// Function to generate ID from text (matches markdown parser behavior)
|
// Function to scroll to element using scrollIntoView
|
||||||
const generateId = (text: string): string => {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to scroll to element
|
|
||||||
const scrollToElement = (element: HTMLElement) => {
|
const scrollToElement = (element: HTMLElement) => {
|
||||||
console.log('Attempting to scroll to element:', element.textContent);
|
// Get comprehensive element information
|
||||||
|
const documentHeight = document.documentElement.scrollHeight;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
let attempts = 0;
|
// Get the absolute position of the element by temporarily scrolling to top
|
||||||
const maxAttempts = 20; // 1 second max wait time
|
const currentScrollY = window.scrollY;
|
||||||
|
let elementTop = 0;
|
||||||
|
|
||||||
// Wait for the element to be properly positioned in the DOM
|
// If we're not at the top, scroll to top temporarily to get accurate positions
|
||||||
const waitForElementPosition = () => {
|
if (currentScrollY > 0) {
|
||||||
attempts++;
|
// Temporarily scroll to top to get accurate element positions
|
||||||
const rect = element.getBoundingClientRect();
|
window.scrollTo(0, 0);
|
||||||
console.log('Element rect (attempt', attempts, '):', rect);
|
|
||||||
|
|
||||||
// If the element has no dimensions, wait a bit more
|
// Wait a moment for the scroll to complete, then measure
|
||||||
if ((rect.height === 0 && rect.width === 0) && attempts < maxAttempts) {
|
setTimeout(() => {
|
||||||
console.log('Element not positioned yet, waiting... (attempt', attempts, ')');
|
const rect = element.getBoundingClientRect();
|
||||||
setTimeout(waitForElementPosition, 50);
|
elementTop = rect.top;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've tried too many times, use fallback method
|
|
||||||
if (attempts >= maxAttempts) {
|
|
||||||
console.log('Max attempts reached, using fallback scroll method');
|
|
||||||
element.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply offset after scroll
|
// Restore original scroll position
|
||||||
setTimeout(() => {
|
window.scrollTo(0, currentScrollY);
|
||||||
const isDesktop = window.innerWidth >= 640;
|
|
||||||
const scrollOffset = isDesktop ? 120 : 100;
|
// Now perform the actual scroll to the target
|
||||||
window.scrollBy({
|
performActualScroll(elementTop);
|
||||||
top: -scrollOffset,
|
}, 50);
|
||||||
behavior: 'smooth'
|
return;
|
||||||
});
|
} else {
|
||||||
}, 100);
|
// We're already at the top, get position directly
|
||||||
return;
|
const rect = element.getBoundingClientRect();
|
||||||
}
|
elementTop = rect.top;
|
||||||
|
performActualScroll(elementTop);
|
||||||
console.log('Element offsetTop:', element.offsetTop);
|
}
|
||||||
console.log('Current scroll position:', window.scrollY);
|
|
||||||
|
function performActualScroll(elementTop: number) {
|
||||||
const isDesktop = window.innerWidth >= 640;
|
console.log('Element details:', {
|
||||||
const scrollOffset = isDesktop ? 120 : 100;
|
elementText: element.textContent?.substring(0, 50),
|
||||||
|
elementId: element.id,
|
||||||
// Use offsetTop which is more reliable for positioned elements
|
elementTop,
|
||||||
const elementTop = element.offsetTop - scrollOffset;
|
currentScrollY: window.scrollY,
|
||||||
|
documentHeight,
|
||||||
console.log('Target scroll position:', elementTop);
|
windowHeight,
|
||||||
console.log('Scroll offset used:', scrollOffset);
|
canScroll: documentHeight > windowHeight
|
||||||
|
|
||||||
// Perform the scroll
|
|
||||||
window.scrollTo({
|
|
||||||
top: elementTop,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Scroll command executed');
|
// Check if page is scrollable
|
||||||
};
|
if (documentHeight <= windowHeight) {
|
||||||
|
console.warn('Page is not tall enough to scroll');
|
||||||
// Start the positioning check
|
return;
|
||||||
waitForElementPosition();
|
}
|
||||||
|
|
||||||
|
// Calculate the target scroll position
|
||||||
|
const offset = 100; // Account for sticky header
|
||||||
|
const targetScrollY = Math.max(0, elementTop - offset);
|
||||||
|
|
||||||
|
console.log('Scroll calculation:', {
|
||||||
|
elementTop,
|
||||||
|
targetScrollY,
|
||||||
|
offset,
|
||||||
|
currentScrollY: window.scrollY,
|
||||||
|
scrollDifference: targetScrollY - window.scrollY
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if we need to scroll at all
|
||||||
|
if (Math.abs(window.scrollY - targetScrollY) < 10) {
|
||||||
|
console.log('Element already at target position, no scroll needed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a simple, reliable scroll method
|
||||||
|
console.log(`Scrolling from ${window.scrollY} to ${targetScrollY}`);
|
||||||
|
|
||||||
|
// Use requestAnimationFrame for smooth scrolling
|
||||||
|
const startScrollY = window.scrollY;
|
||||||
|
const scrollDistance = targetScrollY - startScrollY;
|
||||||
|
const duration = 500; // 500ms
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
const animateScroll = (currentTime: number) => {
|
||||||
|
const elapsed = currentTime - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
|
||||||
|
// Easing function (ease-out)
|
||||||
|
const easeOut = 1 - Math.pow(1 - progress, 3);
|
||||||
|
|
||||||
|
const currentScrollY = startScrollY + (scrollDistance * easeOut);
|
||||||
|
window.scrollTo(0, currentScrollY);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animateScroll);
|
||||||
|
} else {
|
||||||
|
console.log('Scroll animation completed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(animateScroll);
|
||||||
|
|
||||||
|
// Log the scroll after a delay to verify it worked
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Scroll verification - new scrollY:', window.scrollY);
|
||||||
|
console.log('Scroll difference:', Math.abs(window.scrollY - targetScrollY));
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to find element by ID or generated ID
|
// Function to find and scroll to element with retry
|
||||||
const findElement = (id: string): HTMLElement | null => {
|
const findAndScrollToElement = (id: string, retryCount: number = 0) => {
|
||||||
console.log('Looking for element with ID:', id);
|
// First check if the content is rendered
|
||||||
|
const proseContent = document.querySelector('.prose');
|
||||||
// Try direct ID match first
|
if (!proseContent || !proseContent.innerHTML.trim()) {
|
||||||
let element = document.getElementById(id);
|
if (retryCount < 10) {
|
||||||
|
console.log(`Content not yet rendered, retrying... (${retryCount + 1}/10)`);
|
||||||
if (element) {
|
setTimeout(() => {
|
||||||
console.log('Found element by direct ID:', element.textContent);
|
findAndScrollToElement(id, retryCount + 1);
|
||||||
return element;
|
}, 100);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.warn('Content not rendered after retries');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find by generated ID from all headings
|
|
||||||
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
||||||
console.log('Found', headings.length, 'headings on page');
|
|
||||||
|
|
||||||
const found = Array.from(headings).find(heading => {
|
|
||||||
const headingId = generateId(heading.textContent || '');
|
|
||||||
console.log('Checking heading:', heading.textContent, '-> ID:', headingId, 'vs target:', id);
|
|
||||||
return headingId === id;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
console.log('Found element by generated ID:', found.textContent);
|
|
||||||
return found as HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Element not found for ID:', id);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to handle hash-based scrolling
|
const element = document.getElementById(id);
|
||||||
const handleHashScroll = () => {
|
|
||||||
if (!window.location.hash) return;
|
|
||||||
|
|
||||||
const id = window.location.hash.substring(1);
|
|
||||||
console.log('Handling hash scroll for:', id);
|
|
||||||
|
|
||||||
const element = findElement(id);
|
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
console.log('Found element for hash scroll:', element.textContent);
|
console.log('Found target element:', element.textContent?.substring(0, 50));
|
||||||
setTimeout(() => scrollToElement(element), 100);
|
scrollToElement(element);
|
||||||
|
} else if (retryCount < 5) {
|
||||||
|
console.log(`Element not found for anchor: ${id}, retrying... (${retryCount + 1}/5)`);
|
||||||
|
// Retry after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
findAndScrollToElement(id, retryCount + 1);
|
||||||
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
console.log('Element not found for hash:', id);
|
console.warn('Target element not found for anchor after retries:', id);
|
||||||
|
// Log all available IDs for debugging
|
||||||
|
const allIds = Array.from(document.querySelectorAll('[id]')).map(el => el.id);
|
||||||
|
console.log('Available IDs on page:', allIds);
|
||||||
|
|
||||||
|
// Show a user-friendly error message
|
||||||
|
const linkElement = document.querySelector(`a[href="#${id}"]`) as HTMLElement;
|
||||||
|
if (linkElement) {
|
||||||
|
linkElement.setAttribute('title', `Heading "${id}" not found`);
|
||||||
|
linkElement.style.color = '#ef4444'; // Red color for broken links
|
||||||
|
linkElement.style.textDecoration = 'line-through';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -163,48 +190,211 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
|
|||||||
|
|
||||||
if (!link || !link.getAttribute('href')?.startsWith('#')) return;
|
if (!link || !link.getAttribute('href')?.startsWith('#')) return;
|
||||||
|
|
||||||
|
const href = link.getAttribute('href');
|
||||||
|
const id = href?.substring(1);
|
||||||
|
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
console.log('Anchor click detected:', href);
|
||||||
|
|
||||||
|
// Prevent default behavior first
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const href = link.getAttribute('href')!;
|
|
||||||
const id = href.substring(1);
|
|
||||||
|
|
||||||
console.log('Anchor click detected:', id);
|
// Find the target element and scroll to it
|
||||||
|
findAndScrollToElement(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle hash-based scrolling on page load
|
||||||
|
const handleHashScroll = () => {
|
||||||
|
if (!window.location.hash) return;
|
||||||
|
|
||||||
const element = findElement(id);
|
const id = window.location.hash.substring(1);
|
||||||
|
console.log('Handling hash scroll for:', id);
|
||||||
|
|
||||||
|
// Use a longer delay to ensure DOM is fully rendered
|
||||||
|
setTimeout(() => {
|
||||||
|
findAndScrollToElement(id);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle initial hash scroll
|
||||||
|
handleHashScroll();
|
||||||
|
|
||||||
|
// Add event listener for anchor clicks
|
||||||
|
document.addEventListener('click', handleAnchorClick);
|
||||||
|
|
||||||
|
// Add a test function to the window object for debugging
|
||||||
|
(window as any).testScroll = (id: string) => {
|
||||||
|
console.log('Testing scroll to:', id);
|
||||||
|
findAndScrollToElement(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a function to test basic scrolling
|
||||||
|
(window as any).testBasicScroll = () => {
|
||||||
|
console.log('Testing basic scroll functionality');
|
||||||
|
const currentScrollY = window.scrollY;
|
||||||
|
const testScrollY = currentScrollY + 500;
|
||||||
|
|
||||||
|
console.log('Current scrollY:', currentScrollY);
|
||||||
|
console.log('Target scrollY:', testScrollY);
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: testScrollY,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Basic scroll test completed, new scrollY:', window.scrollY);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a function to test scrolling to a specific element
|
||||||
|
(window as any).testElementScroll = (id: string) => {
|
||||||
|
console.log('Testing scroll to element:', id);
|
||||||
|
const element = document.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
console.log('Found element for anchor click:', element.textContent);
|
console.log('Element found, testing scroll...');
|
||||||
scrollToElement(element);
|
scrollToElement(element);
|
||||||
|
|
||||||
// Update URL without reload
|
|
||||||
history.replaceState(null, '', href);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Element not found for anchor:', id);
|
console.log('Element not found:', id);
|
||||||
|
(window as any).listIds();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add IDs to headings that don't have them
|
// Add a simple test function
|
||||||
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
(window as any).runAnchorTest = () => {
|
||||||
console.log('Processing', headings.length, 'headings for ID assignment');
|
console.log('Running anchor link test...');
|
||||||
headings.forEach(heading => {
|
|
||||||
if (!heading.id) {
|
// Test 1: Check if we can find the "overview" heading
|
||||||
const id = generateId(heading.textContent || '');
|
const overviewElement = document.getElementById('overview');
|
||||||
heading.id = id;
|
if (overviewElement) {
|
||||||
console.log('Added ID to heading:', heading.textContent, '->', id);
|
console.log('✅ Found overview element, testing scroll...');
|
||||||
|
scrollToElement(overviewElement);
|
||||||
} else {
|
} else {
|
||||||
console.log('Heading already has ID:', heading.textContent, '->', heading.id);
|
console.log('❌ Overview element not found');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Test 2: Check if we can find the "test-heading" element
|
||||||
|
setTimeout(() => {
|
||||||
|
const testHeadingElement = document.getElementById('test-heading');
|
||||||
|
if (testHeadingElement) {
|
||||||
|
console.log('✅ Found test-heading element, testing scroll...');
|
||||||
|
scrollToElement(testHeadingElement);
|
||||||
|
} else {
|
||||||
|
console.log('❌ Test-heading element not found');
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle initial hash scroll
|
// Add a function to list all available IDs
|
||||||
setTimeout(handleHashScroll, 100);
|
(window as any).listIds = () => {
|
||||||
|
const allIds = Array.from(document.querySelectorAll('[id]')).map(el => ({
|
||||||
|
id: el.id,
|
||||||
|
text: el.textContent?.substring(0, 50),
|
||||||
|
tag: el.tagName
|
||||||
|
}));
|
||||||
|
console.log('Available IDs on page:', allIds);
|
||||||
|
return allIds;
|
||||||
|
};
|
||||||
|
|
||||||
// Add event listeners
|
// Add a function to debug anchor links
|
||||||
document.addEventListener('click', handleAnchorClick);
|
(window as any).debugAnchors = () => {
|
||||||
window.addEventListener('hashchange', handleHashScroll);
|
console.log('=== Anchor Link Debug ===');
|
||||||
|
|
||||||
|
// Get all anchor links
|
||||||
|
const anchorLinks = Array.from(document.querySelectorAll('a[href^="#"]')).map(el => ({
|
||||||
|
href: el.getAttribute('href'),
|
||||||
|
text: el.textContent,
|
||||||
|
targetId: el.getAttribute('href')?.substring(1)
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('Anchor links found:', anchorLinks);
|
||||||
|
|
||||||
|
// Get all headings with IDs
|
||||||
|
const headings = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')).map(el => ({
|
||||||
|
id: el.id,
|
||||||
|
text: el.textContent?.substring(0, 50),
|
||||||
|
tag: el.tagName,
|
||||||
|
offsetTop: (el as HTMLElement).offsetTop,
|
||||||
|
getBoundingClientRect: (el as HTMLElement).getBoundingClientRect()
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('Headings with IDs:', headings);
|
||||||
|
|
||||||
|
// Check which anchor links have matching headings
|
||||||
|
anchorLinks.forEach(link => {
|
||||||
|
const hasMatch = headings.some(h => h.id === link.targetId);
|
||||||
|
const status = hasMatch ? '✅' : '❌';
|
||||||
|
console.log(`${status} [${link.text}](#${link.targetId}) -> ${hasMatch ? 'FOUND' : 'NOT FOUND'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== End Debug ===');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a function to show element positions
|
||||||
|
(window as any).showPositions = () => {
|
||||||
|
console.log('=== Element Positions ===');
|
||||||
|
|
||||||
|
const headings = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'));
|
||||||
|
|
||||||
|
headings.forEach((el, index) => {
|
||||||
|
const element = el as HTMLElement;
|
||||||
|
|
||||||
|
// Calculate absolute position
|
||||||
|
let elementTop = 0;
|
||||||
|
let currentElement = element;
|
||||||
|
while (currentElement && currentElement !== document.body) {
|
||||||
|
elementTop += currentElement.offsetTop;
|
||||||
|
currentElement = currentElement.offsetParent as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${index + 1}. ${element.textContent?.substring(0, 30)}:`);
|
||||||
|
console.log(` ID: ${element.id}`);
|
||||||
|
console.log(` Calculated elementTop: ${elementTop}`);
|
||||||
|
console.log(` element.offsetTop: ${element.offsetTop}`);
|
||||||
|
console.log(` Current scrollY: ${window.scrollY}`);
|
||||||
|
console.log(` Would scroll to: ${Math.max(0, elementTop - 100)}`);
|
||||||
|
console.log('---');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== End Positions ===');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a simple test function
|
||||||
|
(window as any).testScrollToElement = (id: string) => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
console.log(`Testing scroll to ${id}...`);
|
||||||
|
|
||||||
|
// Calculate position the same way as scrollToElement
|
||||||
|
let elementTop = 0;
|
||||||
|
let currentElement = element;
|
||||||
|
while (currentElement && currentElement !== document.body) {
|
||||||
|
elementTop += currentElement.offsetTop;
|
||||||
|
currentElement = currentElement.offsetParent as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetScrollY = Math.max(0, elementTop - 100);
|
||||||
|
console.log(`Element ${id} is at position ${elementTop}, would scroll to ${targetScrollY}`);
|
||||||
|
console.log(`Current scroll position: ${window.scrollY}`);
|
||||||
|
|
||||||
|
// Perform the scroll
|
||||||
|
scrollToElement(element);
|
||||||
|
} else {
|
||||||
|
console.log(`Element with id "${id}" not found`);
|
||||||
|
(window as any).listIds();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('click', handleAnchorClick);
|
document.removeEventListener('click', handleAnchorClick);
|
||||||
window.removeEventListener('hashchange', handleHashScroll);
|
delete (window as any).testScroll;
|
||||||
|
delete (window as any).testBasicScroll;
|
||||||
|
delete (window as any).testElementScroll;
|
||||||
|
delete (window as any).listIds;
|
||||||
|
delete (window as any).debugAnchors;
|
||||||
|
delete (window as any).showPositions;
|
||||||
|
delete (window as any).testScrollToElement;
|
||||||
};
|
};
|
||||||
}, [post]);
|
}, [post]);
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,89 @@ function generateId(text: string): string {
|
|||||||
.replace(/^-+|-+$/g, '');
|
.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();
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
// Custom heading renderer to add IDs
|
// Custom heading renderer to add IDs
|
||||||
renderer.heading = (text, level) => {
|
renderer.heading = (text, level) => {
|
||||||
const id = generateId(text);
|
const id = slugify(text);
|
||||||
return `<h${level} id="${id}">${text}</h${level}>`;
|
return `<h${level} id="${id}">${text}</h${level}>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,7 +146,12 @@ export async function getPostBySlug(slug: string): Promise<Post> {
|
|||||||
|
|
||||||
let processedContent = '';
|
let processedContent = '';
|
||||||
try {
|
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 window = new JSDOM('').window;
|
||||||
const purify = DOMPurify(window);
|
const purify = DOMPurify(window);
|
||||||
processedContent = purify.sanitize(rawHtml as string, {
|
processedContent = purify.sanitize(rawHtml as string, {
|
||||||
|
|||||||
Reference in New Issue
Block a user