/** * MDX processing utilities for enhanced markdown content */ /** * Format date consistently across the markdown pipeline * Uses "Month Year" format (e.g., "April 2025") */ export function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString("en-US", { year: "numeric", month: "long", }); } /** * Process markdown content and extract metadata * @param {string} markdown - Raw markdown content * @returns {object} Processed content with metadata */ export function processMarkdown(markdown) { if (!markdown) { return { content: "", htmlContent: "", wordCount: 0, readingTime: 0, headings: [], links: [], images: [], }; } // Extract headings for table of contents const headings = extractHeadings(markdown); // Extract links const links = extractLinks(markdown); // Extract images const images = extractImages(markdown); // Calculate word count and reading time const wordCount = calculateWordCount(markdown); const readingTime = calculateReadingTime(wordCount); // Convert markdown to HTML const htmlContent = markdownToHtml(markdown); return { content: markdown, htmlContent, wordCount, readingTime, headings, links, images, }; } /** * Extract all headings from markdown content * @param {string} markdown - Raw markdown content * @returns {Array} Array of heading objects with level, text, and id */ function extractHeadings(markdown) { const headingRegex = /^(#{1,6})\s+(.+)$/gm; const headings = []; let match; while ((match = headingRegex.exec(markdown)) !== null) { const level = match[1].length; const text = match[2].trim(); const id = generateHeadingId(text); headings.push({ level, text, id, line: markdown.substring(0, match.index).split("\n").length, }); } return headings; } /** * Extract all links from markdown content * @param {string} markdown - Raw markdown content * @returns {Array} Array of link objects */ function extractLinks(markdown) { const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; const links = []; let match; while ((match = linkRegex.exec(markdown)) !== null) { links.push({ text: match[1], url: match[2], index: match.index, }); } return links; } /** * Extract all images from markdown content * @param {string} markdown - Raw markdown content * @returns {Array} Array of image objects */ function extractImages(markdown) { const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; const images = []; let match; while ((match = imageRegex.exec(markdown)) !== null) { images.push({ alt: match[1], src: match[2], index: match.index, }); } return images; } /** * Generate a unique ID for a heading * @param {string} text - Heading text * @returns {string} Unique ID */ function generateHeadingId(text) { return text .toLowerCase() .replace(/[^\w\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-") .trim(); } /** * Calculate word count from markdown content * @param {string} markdown - Raw markdown content * @returns {number} Word count */ function calculateWordCount(markdown) { // Remove markdown syntax and count words const cleanText = markdown .replace(/[#*`~\[\]()]/g, "") // Remove markdown characters .replace(/\n+/g, " ") // Replace newlines with spaces .trim(); return cleanText.split(/\s+/).filter((word) => word.length > 0).length; } /** * Calculate estimated reading time * @param {number} wordCount - Number of words * @returns {number} Reading time in minutes */ function calculateReadingTime(wordCount) { const wordsPerMinute = 200; // Average reading speed return Math.ceil(wordCount / wordsPerMinute); } /** * Convert markdown to HTML with enhanced formatting * @param {string} markdown - Raw markdown content * @returns {string} HTML content */ function markdownToHtml(markdown) { if (!markdown) return ""; return ( markdown // Headers with IDs .replace(/^### (.*$)/gim, (match, text) => { const id = generateHeadingId(text); return `

${text}

`; }) .replace(/^## (.*$)/gim, (match, text) => { const id = generateHeadingId(text); return `

${text}

`; }) .replace(/^# (.*$)/gim, (match, text) => { const id = generateHeadingId(text); return `

${text}

`; }) // Bold and italic .replace(/\*\*(.*?)\*\*/g, "$1") .replace(/\*(.*?)\*/g, "$1") // Code blocks .replace( /```(\w+)?\n([\s\S]*?)\n```/g, '
$2
' ) .replace(/`([^`]+)`/g, "$1") // Links .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // Lists .replace(/^\* (.*$)/gim, "
  • $1
  • ") .replace(/^- (.*$)/gim, "
  • $1
  • ") .replace(/(
  • .*<\/li>)/gim, "") // Blockquotes .replace(/^> (.*$)/gim, "

    $1

    ") // Horizontal rules .replace(/^---$/gm, "
    ") .replace(/^\*\*\*$/gm, "
    ") // Paragraphs .replace(/\n\n/g, "

    ") .replace(/^(?!<[h|u|li|blockquote|hr|pre])(.*$)/gim, "

    $1

    ") // Clean up empty paragraphs and fix list wrapping .replace(/

    <\/p>/g, "") .replace(/

    (.*?)<\/p>/g, (match, content) => { return content.trim() ? match : ""; }) .replace(/<\/ul>\s*