From 69e6336d5c8a3d8912df4d0b15011cc798aa8d54 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Sat, 21 Jun 2025 22:12:12 +0200 Subject: [PATCH] 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. --- posts/anchor-test.md | 101 +++++++ posts/mdtest.md | 25 +- src/app/globals.css | 41 +++ src/app/posts/[...slug]/page.tsx | 450 ++++++++++++++++++++++--------- src/lib/markdown.ts | 87 +++++- 5 files changed, 556 insertions(+), 148 deletions(-) create mode 100644 posts/anchor-test.md diff --git a/posts/anchor-test.md b/posts/anchor-test.md new file mode 100644 index 0000000..b02277b --- /dev/null +++ b/posts/anchor-test.md @@ -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! \ No newline at end of file diff --git a/posts/mdtest.md b/posts/mdtest.md index 945a490..1b6008b 100644 --- a/posts/mdtest.md +++ b/posts/mdtest.md @@ -11,23 +11,16 @@ author: Rattatwinko's * [Overview](#overview) * [Philosophy](#philosophy) - * [Inline HTML](#html) - * [Automatic Escaping for Special Characters](#autoescape) -* [Block Elements](#block) - * [Paragraphs and Line Breaks](#p) - * [Headers](#header) - * [Blockquotes](#blockquote) - * [Lists](#list) - * [Code Blocks](#precode) - * [Horizontal Rules](#hr) -* [Span Elements](#span) - * [Links](#link) - * [Emphasis](#em) +* [Block Elements](#block-elements) + * [Paragraphs and Line Breaks](#paragraphs-and-line-breaks) + * [Headers](#headers) + * [Blockquotes](#blockquotes) + * [Lists](#lists) + * [Code Blocks](#code-blocks) +* [Span Elements](#span-elements) + * [Links](#links) + * [Emphasis](#emphasis) * [Code](#code) - * [Images](#img) -* [Miscellaneous](#misc) - * [Backslash Escapes](#backslash) - * [Automatic Links](#autolink) **Note:** This document is itself written using Markdown; you diff --git a/src/app/globals.css b/src/app/globals.css index bb04461..334b354 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -57,6 +57,47 @@ html { 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 { color: #111827; font-weight: 700; diff --git a/src/app/posts/[...slug]/page.tsx b/src/app/posts/[...slug]/page.tsx index 2410452..14a2365 100644 --- a/src/app/posts/[...slug]/page.tsx +++ b/src/app/posts/[...slug]/page.tsx @@ -35,124 +35,151 @@ export default function PostPage({ params }: { params: { slug: string[] } }) { useEffect(() => { if (!post) return; - // Function to generate ID from text (matches markdown parser behavior) - const generateId = (text: string): string => { - return text - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); - }; - - // Function to scroll to element + // Function to scroll to element using scrollIntoView 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; - const maxAttempts = 20; // 1 second max wait time + // Get the absolute position of the element by temporarily scrolling to top + const currentScrollY = window.scrollY; + let elementTop = 0; - // Wait for the element to be properly positioned in the DOM - const waitForElementPosition = () => { - attempts++; - const rect = element.getBoundingClientRect(); - console.log('Element rect (attempt', attempts, '):', rect); + // If we're not at the top, scroll to top temporarily to get accurate positions + if (currentScrollY > 0) { + // Temporarily scroll to top to get accurate element positions + window.scrollTo(0, 0); - // If the element has no dimensions, wait a bit more - if ((rect.height === 0 && rect.width === 0) && attempts < maxAttempts) { - console.log('Element not positioned yet, waiting... (attempt', attempts, ')'); - setTimeout(waitForElementPosition, 50); - 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' - }); + // Wait a moment for the scroll to complete, then measure + setTimeout(() => { + const rect = element.getBoundingClientRect(); + elementTop = rect.top; - // Apply offset after scroll - setTimeout(() => { - const isDesktop = window.innerWidth >= 640; - const scrollOffset = isDesktop ? 120 : 100; - window.scrollBy({ - top: -scrollOffset, - behavior: 'smooth' - }); - }, 100); - return; - } - - console.log('Element offsetTop:', element.offsetTop); - console.log('Current scroll position:', window.scrollY); - - const isDesktop = window.innerWidth >= 640; - const scrollOffset = isDesktop ? 120 : 100; - - // Use offsetTop which is more reliable for positioned elements - const elementTop = element.offsetTop - scrollOffset; - - console.log('Target scroll position:', elementTop); - console.log('Scroll offset used:', scrollOffset); - - // Perform the scroll - window.scrollTo({ - top: elementTop, - behavior: 'smooth' + // Restore original scroll position + window.scrollTo(0, currentScrollY); + + // Now perform the actual scroll to the target + performActualScroll(elementTop); + }, 50); + return; + } else { + // We're already at the top, get position directly + const rect = element.getBoundingClientRect(); + elementTop = rect.top; + performActualScroll(elementTop); + } + + function performActualScroll(elementTop: number) { + console.log('Element details:', { + elementText: element.textContent?.substring(0, 50), + elementId: element.id, + elementTop, + currentScrollY: window.scrollY, + documentHeight, + windowHeight, + canScroll: documentHeight > windowHeight }); - console.log('Scroll command executed'); - }; - - // Start the positioning check - waitForElementPosition(); + // Check if page is scrollable + if (documentHeight <= windowHeight) { + console.warn('Page is not tall enough to scroll'); + return; + } + + // 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 - const findElement = (id: string): HTMLElement | null => { - console.log('Looking for element with ID:', id); - - // Try direct ID match first - let element = document.getElementById(id); - - if (element) { - console.log('Found element by direct ID:', element.textContent); - return element; + // Function to find and scroll to element with retry + const findAndScrollToElement = (id: string, retryCount: number = 0) => { + // First check if the content is rendered + const proseContent = document.querySelector('.prose'); + if (!proseContent || !proseContent.innerHTML.trim()) { + if (retryCount < 10) { + console.log(`Content not yet rendered, retrying... (${retryCount + 1}/10)`); + setTimeout(() => { + findAndScrollToElement(id, retryCount + 1); + }, 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 handleHashScroll = () => { - if (!window.location.hash) return; - - const id = window.location.hash.substring(1); - console.log('Handling hash scroll for:', id); - - const element = findElement(id); - + const element = document.getElementById(id); if (element) { - console.log('Found element for hash scroll:', element.textContent); - setTimeout(() => scrollToElement(element), 100); + console.log('Found target element:', element.textContent?.substring(0, 50)); + 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 { - 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; + 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(); - 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) { - console.log('Found element for anchor click:', element.textContent); + console.log('Element found, testing scroll...'); scrollToElement(element); - - // Update URL without reload - history.replaceState(null, '', href); } 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 - const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); - console.log('Processing', headings.length, 'headings for ID assignment'); - headings.forEach(heading => { - if (!heading.id) { - const id = generateId(heading.textContent || ''); - heading.id = id; - console.log('Added ID to heading:', heading.textContent, '->', id); + // Add a simple test function + (window as any).runAnchorTest = () => { + console.log('Running anchor link test...'); + + // Test 1: Check if we can find the "overview" heading + const overviewElement = document.getElementById('overview'); + if (overviewElement) { + console.log('✅ Found overview element, testing scroll...'); + scrollToElement(overviewElement); } 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 - setTimeout(handleHashScroll, 100); + // Add a function to list all available IDs + (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 - document.addEventListener('click', handleAnchorClick); - window.addEventListener('hashchange', handleHashScroll); + // Add a function to debug anchor links + (window as any).debugAnchors = () => { + 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 () => { 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]); diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 90db1c7..18db88d 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -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 `${text}`; }; @@ -68,7 +146,12 @@ export async function getPostBySlug(slug: string): Promise { 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, {