const express = require('express'); const router = express.Router(); const puppeteer = require('puppeteer'); const path = require('path'); const archiver = require('archiver'); const fs = require('fs'); const { logger } = require('../utils/logger'); // POST /api/export/pdf - Generate PDF from markdown content router.post('/pdf', async (req, res) => { const startTime = Date.now(); const { content, title = 'Design Journal' } = req.body; logger.exportOperation('pdf', 'application/pdf', content.length); if (!content || content.trim() === '') { logger.error('EXPORT', 'PDF export failed: Content required'); return res.status(400).json({ success: false, error: 'Content required to generate PDF' }); } let browser; try { // Convert markdown to HTML with styles const htmlContent = generateStyledHTML(content, title); // Launch Puppeteer browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // Set HTML content await page.setContent(htmlContent, { waitUntil: 'domcontentloaded', timeout: parseInt(process.env.PDF_TIMEOUT) || 30000 }); // Wait for Mermaid to load and diagrams to render await new Promise(resolve => setTimeout(resolve, 2000)); // Check if Mermaid diagrams exist and wait for them to load try { await page.waitForFunction(() => { const mermaidElements = document.querySelectorAll('.mermaid'); if (mermaidElements.length === 0) return true; return Array.from(mermaidElements).every(el => el.querySelector('svg') || el.textContent.trim() === '' || el.classList.contains('error') ); }, { timeout: 10000 }); } catch (e) { console.log('Timeout waiting for Mermaid, proceeding...'); } // Generate PDF const pdfBuffer = await page.pdf({ format: 'A4', margin: { top: '2cm', right: '2cm', bottom: '2cm', left: '2cm' }, printBackground: true, displayHeaderFooter: true, headerTemplate: `
${escapeHtml(code)}`;
});
// Inline code
html = html.replace(/`([^`]+)`/g, '$1');
// Enhanced table processing
html = html.replace(/(\|.*\|\s*\n)+/g, (match) => {
const lines = match.trim().split('\n').filter(line => line.trim());
let tableHtml = '$1') // Bold and italic (order matters) .replace(/\*\*\*(.*?)\*\*\*/g, '$1') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') // Strikethrough .replace(/~~(.*?)~~/g, '
.*?<\/blockquote>\s*)+/gs, (match) => { const content = match.replace(/<\/?blockquote>/g, ''); return '' + content + ''; }); // Process paragraphs html = html.split('\n\n').map(paragraph => { paragraph = paragraph.trim(); if (!paragraph) return ''; // Don't wrap block elements in paragraphs if (paragraph.match(/^<(h[1-6]|div|table|ul|ol|blockquote|hr|pre)/)) { return paragraph; } return '' + paragraph.replace(/\n/g, '
'; }).join('\n\n'); const styledHTML = `
') + '${title} ${html} `; return styledHTML; } // Function to sanitize filename function sanitizeFilename(filename) { return filename .replace(/[^\w\s-]/g, '') // Remove special characters .replace(/\s+/g, '-') // Replace spaces with dashes .toLowerCase() // Lowercase .substring(0, 100); // Limit length } // POST /api/export/web - Generate ZIP with HTML + CSS router.post('/web', async (req, res) => { const startTime = Date.now(); const { content, title = 'Design Journal' } = req.body; logger.exportOperation('web-zip', 'application/zip', content.length); if (!content || content.trim() === '') { logger.error('EXPORT', 'Web export failed: Content required'); return res.status(400).json({ success: false, error: 'Content required to generate web export' }); } try { // Read CSS files const styleCssPath = path.join(__dirname, '../assets/css/style.css'); const githubCssPath = path.join(__dirname, '../assets/css/github-preview.css'); let styleCSS = ''; let githubCSS = ''; try { styleCSS = fs.readFileSync(styleCssPath, 'utf8'); githubCSS = fs.readFileSync(githubCssPath, 'utf8'); } catch (err) { console.error('Error reading CSS files:', err); } // Generate HTML content const htmlContent = `${title} `; // Combine CSS with additional presentation styles const combinedCSS = ` ${styleCSS} ${githubCSS} /* Additional presentation styles */ body { margin: 0; padding: 0; } #presentation-container { max-width: 900px; margin: 0 auto; padding: 3rem 2rem; min-height: 100vh; } .presentation-header { text-align: center; padding: 2rem 0; border-bottom: 2px solid var(--border-color); margin-bottom: 3rem; } .presentation-header h1 { margin: 0 0 0.5rem 0; color: var(--primary-color); } .presentation-header .date { color: var(--text-light); font-size: 0.9rem; } .presentation-content { line-height: 1.8; } `; // Create ZIP archive const archive = archiver('zip', { zlib: { level: 9 } // Maximum compression }); res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${sanitizeFilename(title)}-web.zip"`); // Pipe archive to response archive.pipe(res); // Add files to archive archive.append(htmlContent, { name: 'index.html' }); archive.append(combinedCSS, { name: 'style.css' }); // Finalize archive await archive.finalize(); logger.info('EXPORT', `Web ZIP generated successfully in ${Date.now() - startTime}ms`, { title }); } catch (error) { logger.error('EXPORT', 'Web export error', { error: error.message, title }); res.status(500).json({ success: false, error: 'Error during web export' }); } }); module.exports = router;${title}
${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}