const express = require('express'); const router = express.Router(); const puppeteer = require('puppeteer'); const path = require('path'); const archiver = require('archiver'); const fs = require('fs'); // POST /api/export/pdf - Generate PDF from markdown content router.post('/pdf', async (req, res) => { const { content, title = 'Design Journal' } = req.body; if (!content || content.trim() === '') { 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: `
${title}
`, footerTemplate: `
Page of - Generated on ${new Date().toLocaleDateString('en-US')}
` }); await browser.close(); // Check PDF size const maxSize = parseInt(process.env.PDF_MAX_SIZE) || 10485760; // 10MB default if (pdfBuffer.length > maxSize) { return res.status(413).json({ success: false, error: 'Generated PDF exceeds maximum allowed size' }); } // Send PDF res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="${sanitizeFilename(title)}.pdf"`); res.setHeader('Content-Length', pdfBuffer.length); res.send(pdfBuffer); } catch (error) { console.error('PDF generation error:', error); if (browser) { await browser.close(); } res.status(500).json({ success: false, error: 'Error during PDF generation' }); } }); // Utility function to escape HTML function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } // Function to convert markdown to styled HTML with Mermaid support function generateStyledHTML(content, title) { let html = content; // Process Mermaid diagrams BEFORE code blocks html = html.replace(/```mermaid\n([\s\S]*?)```/g, '
$1
'); // Process code blocks with language html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { const language = lang ? ` class="language-${lang}"` : ''; return `
${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 = '\n'; let inHeader = true; lines.forEach((line, index) => { // Ignore separator lines if (line.match(/^\s*\|[\s\-:]*\|\s*$/)) { inHeader = false; return; } const cells = line.split('|') .map(cell => cell.trim()) .filter((cell, idx, arr) => idx > 0 && idx < arr.length - 1); if (cells.length === 0) return; const tag = inHeader ? 'th' : 'td'; tableHtml += ' \n'; cells.forEach(cell => { tableHtml += ` <${tag}>${cell}\n`; }); tableHtml += ' \n'; if (inHeader && index === 0) inHeader = false; }); tableHtml += '
\n'; return tableHtml; }); // Markdown to HTML conversion html = html // Headings (in descending order) .replace(/^#{6}\s+(.*$)/gm, '
$1
') .replace(/^#{5}\s+(.*$)/gm, '
$1
') .replace(/^#{4}\s+(.*$)/gm, '

$1

') .replace(/^#{3}\s+(.*$)/gm, '

$1

') .replace(/^#{2}\s+(.*$)/gm, '

$1

') .replace(/^#{1}\s+(.*$)/gm, '

$1

') // Blockquotes .replace(/^>\s+(.*$)/gm, '
$1
') // Bold and italic (order matters) .replace(/\*\*\*(.*?)\*\*\*/g, '$1') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') // Strikethrough .replace(/~~(.*?)~~/g, '$1') // Numbered lists .replace(/^(\s*)(\d+)\. (.*$)/gm, '
  • $3
  • ') // Bullet lists and tasks .replace(/^(\s*)- \[x\] (.*$)/gm, '
  • $2
  • ') .replace(/^(\s*)- \[ \] (.*$)/gm, '
  • $2
  • ') .replace(/^(\s*)[*\-+] (.*$)/gm, '
  • $2
  • ') // Links .replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1') // Horizontal rule .replace(/^---+$/gm, '
    '); // Group consecutive lists html = html.replace(/(]*>.*?<\/li>\s*)+/gs, (match) => { if (match.includes('class="numbered"')) { return '
      ' + match.replace(/ class="numbered"/g, '') + '
    '; } else { return ''; } }); // Clean up consecutive multiple lists html = html.replace(/(<\/[uo]l>\s*<[uo]l>)/g, ''); // Group consecutive blockquotes html = html.replace(/(
    .*?<\/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 { content, title = 'Design Journal' } = req.body; if (!content || content.trim() === '') { 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}

    ${title}

    ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
    `; // 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(); } catch (error) { console.error('Web export error:', error); res.status(500).json({ success: false, error: 'Error during web export' }); } }); module.exports = router;