diff --git a/server/utils/markdownParser.js b/server/utils/markdownParser.js new file mode 100644 index 0000000..d30013b --- /dev/null +++ b/server/utils/markdownParser.js @@ -0,0 +1,110 @@ +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 +}; \ No newline at end of file