const fs = require('fs-extra'); const path = require('path'); const matter = require('gray-matter'); const marked = require('marked'); marked.setOptions({ breaks: true, gfm: true }); /** * @param {string} filePath - Path to markdown file * @returns {Object} Parsed data with frontmatter and HTML content */ const parseMarkdownFile = async (filePath) => { try { const fileContent = await fs.readFile(filePath, 'utf-8'); const { data, content } = matter(fileContent); const html = marked.parse(content); return { ...data, content: html, excerpt: generateExcerpt(content), slug: path.basename(filePath, '.md'), date: data.date || new Date().toISOString() }; } catch (error) { console.error(`Error parsing mardown file ${filePath}:`, error); throw error; } }; /** * Generate an excerpt from markdown content * @param {string} content - Markdown content * @param {number} length - Length of excerpt in characters * @returns {string} Plain text excerpt */ const generateExcerpt = (content, length = 200) => { // Convert markdown to plain text for excerpt const plainText = content .replace(/#+\s+(.*)/g, '$1') // Remove heading markers .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Extract link text .replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold markers .replace(/\*([^*]+)\*/g, '$1') // Remove italic markers .replace(/`([^`]+)`/g, '$1') // Remove code markers .replace(/```[\s\S]+?```/g, '') // Remove code blocks .replace(/\n/g, ' ') // Replace newlines with spaces .trim(); return plainText.length > length ? plainText.substring(0, length) + '...' : plainText; }; /** * Get all blog posts, optionally filtered by draft status * @param {boolean} includeDrafts - Whether to include draft posts * @returns {Array} Array of parsed blog posts */ const getAllPosts = async (includeDrafts = false) => { try { const postsDir = path.join(__dirname, '../content/posts'); const files = await fs.readdir(postsDir); const posts = await Promise.all( files .filter(file => file.endsWith('.md')) .map(async (file) => { const filePath = path.join(postsDir, file); return await parseMarkdownFile(filePath); }) ); // Filter out drafts if needed const filteredPosts = includeDrafts ? posts : posts.filter(post => !post.draft); // Sort by date, newest first return filteredPosts.sort((a, b) => new Date(b.date) - new Date(a.date)); } catch (error) { console.error('Error getting all posts:', error); throw error; } }; /** * Get a single blog post by slug * @param {string} slug - Post slug * @returns {Object} Parsed blog post */ const getPostBySlug = async (slug) => { try { const postsDir = path.join(__dirname, '../content/posts'); const filePath = path.join(postsDir, `${slug}.md`); return await parseMarkdownFile(filePath); } catch (error) { console.error(`Error getting post with slug ${slug}:`, error); throw error; } }; module.exports = { parseMarkdownFile, getAllPosts, getPostBySlug };