react-personal-website/server/utils/markdownParser.js
2025-02-28 11:28:51 -05:00

110 lines
No EOL
3.1 KiB
JavaScript

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
};