Major Features:
- Enhanced Mode v2: Smart Adaptive section selection strategy
* AI automatically selects ONE optimal section per iteration
* Intelligent selection based on quality assessment and strategic importance
* Section tracking to avoid duplicate modifications
* Fixed header level preservation (## stays ##, ### stays ###)
* Updated document code block format (document)
- Mermaid Auto-Fix System
* New POST /api/ai/fix-mermaid endpoint for automatic diagram correction
* Automatic error detection in preview and presentation modes
* Inter-window communication for seamless editor updates
* AI-powered syntax error resolution
- Comprehensive Logging System
* Centralized logger utility (utils/logger.js)
* API request/response middleware logging
* AI operations: rephrase, check-inconsistencies, check-duplications, give-advice, liberty-mode, fix-mermaid
* Storage operations: create, read, update, delete journals
* Export operations: PDF and Web ZIP generation
* Structured logging with timestamps, levels, categories, and metadata
Improvements:
- Table of Contents: Clean display with markdown formatting stripped from titles
- Section badges: Fixed visibility with hardcoded blue background (#2563eb)
- Internationalization: Complete English translation (lang, UI text, placeholders, comments)
- Code cleanup: Removed all emojis from codebase
Technical Changes:
- routes/ai.js: Enhanced mode implementation, Mermaid fix endpoint, comprehensive logging
- routes/api.js: Storage operation logging
- routes/export.js: Export operation logging
- routes/index.js: Presentation mode Mermaid auto-fix
- assets/js/app.js: TOC markdown stripping, Mermaid error handling, message listeners
- assets/css/style.css: Dark mode comment, English placeholders, header icon
- views/page.js: English lang attribute, favicon, scroll-to-top button
- views/header.js: Theme toggle icon
- utils/logger.js: New centralized logging system
- app.js: API request/response logging middleware
686 lines
17 KiB
JavaScript
686 lines
17 KiB
JavaScript
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: `
|
|
<div style="font-size: 10px; width: 100%; text-align: center; color: #666; margin-top: 1cm;">
|
|
<span>${title}</span>
|
|
</div>
|
|
`,
|
|
footerTemplate: `
|
|
<div style="font-size: 10px; width: 100%; text-align: center; color: #666; margin-bottom: 1cm;">
|
|
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span> - Generated on ${new Date().toLocaleDateString('en-US')}</span>
|
|
</div>
|
|
`
|
|
});
|
|
|
|
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);
|
|
|
|
logger.info('EXPORT', `PDF generated successfully in ${Date.now() - startTime}ms`, { size: pdfBuffer.length, title });
|
|
|
|
res.send(pdfBuffer);
|
|
|
|
} catch (error) {
|
|
logger.error('EXPORT', 'PDF generation error', { error: error.message, title });
|
|
|
|
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, '<div class="mermaid">$1</div>');
|
|
|
|
// Process code blocks with language
|
|
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
|
const language = lang ? ` class="language-${lang}"` : '';
|
|
return `<pre><code${language}>${escapeHtml(code)}</code></pre>`;
|
|
});
|
|
|
|
// Inline code
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
|
|
// Enhanced table processing
|
|
html = html.replace(/(\|.*\|\s*\n)+/g, (match) => {
|
|
const lines = match.trim().split('\n').filter(line => line.trim());
|
|
let tableHtml = '<table>\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 += ' <tr>\n';
|
|
cells.forEach(cell => {
|
|
tableHtml += ` <${tag}>${cell}</${tag}>\n`;
|
|
});
|
|
tableHtml += ' </tr>\n';
|
|
|
|
if (inHeader && index === 0) inHeader = false;
|
|
});
|
|
|
|
tableHtml += '</table>\n';
|
|
return tableHtml;
|
|
});
|
|
|
|
// Markdown to HTML conversion
|
|
html = html
|
|
// Headings (in descending order)
|
|
.replace(/^#{6}\s+(.*$)/gm, '<h6>$1</h6>')
|
|
.replace(/^#{5}\s+(.*$)/gm, '<h5>$1</h5>')
|
|
.replace(/^#{4}\s+(.*$)/gm, '<h4>$1</h4>')
|
|
.replace(/^#{3}\s+(.*$)/gm, '<h3>$1</h3>')
|
|
.replace(/^#{2}\s+(.*$)/gm, '<h2>$1</h2>')
|
|
.replace(/^#{1}\s+(.*$)/gm, '<h1>$1</h1>')
|
|
// Blockquotes
|
|
.replace(/^>\s+(.*$)/gm, '<blockquote>$1</blockquote>')
|
|
// Bold and italic (order matters)
|
|
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
// Strikethrough
|
|
.replace(/~~(.*?)~~/g, '<del>$1</del>')
|
|
// Numbered lists
|
|
.replace(/^(\s*)(\d+)\. (.*$)/gm, '<li class="numbered">$3</li>')
|
|
// Bullet lists and tasks
|
|
.replace(/^(\s*)- \[x\] (.*$)/gm, '<li class="todo-done">$2</li>')
|
|
.replace(/^(\s*)- \[ \] (.*$)/gm, '<li class="todo">$2</li>')
|
|
.replace(/^(\s*)[*\-+] (.*$)/gm, '<li>$2</li>')
|
|
// Links
|
|
.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
|
|
// Horizontal rule
|
|
.replace(/^---+$/gm, '<hr>');
|
|
|
|
// Group consecutive lists
|
|
html = html.replace(/(<li[^>]*>.*?<\/li>\s*)+/gs, (match) => {
|
|
if (match.includes('class="numbered"')) {
|
|
return '<ol>' + match.replace(/ class="numbered"/g, '') + '</ol>';
|
|
} else {
|
|
return '<ul>' + match + '</ul>';
|
|
}
|
|
});
|
|
|
|
// Clean up consecutive multiple lists
|
|
html = html.replace(/(<\/[uo]l>\s*<[uo]l>)/g, '');
|
|
|
|
// Group consecutive blockquotes
|
|
html = html.replace(/(<blockquote>.*?<\/blockquote>\s*)+/gs, (match) => {
|
|
const content = match.replace(/<\/?blockquote>/g, '');
|
|
return '<blockquote>' + content + '</blockquote>';
|
|
});
|
|
|
|
// 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 '<p>' + paragraph.replace(/\n/g, '<br>') + '</p>';
|
|
}).join('\n\n');
|
|
|
|
const styledHTML = `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title}</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
|
<script>
|
|
mermaid.initialize({
|
|
startOnLoad: true,
|
|
theme: 'default',
|
|
themeVariables: {
|
|
primaryColor: '#0969da',
|
|
primaryTextColor: '#24292f',
|
|
primaryBorderColor: '#0969da',
|
|
lineColor: '#656d76',
|
|
sectionBkgColor: '#f6f8fa',
|
|
altSectionBkgColor: '#ffffff',
|
|
gridColor: '#d1d9e0',
|
|
secondaryColor: '#f6f8fa',
|
|
tertiaryColor: '#0969da'
|
|
},
|
|
fontFamily: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif',
|
|
fontSize: 14
|
|
});
|
|
</script>
|
|
<style>
|
|
@page {
|
|
size: A4;
|
|
margin: 2cm;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: #24292f;
|
|
font-size: 11pt;
|
|
background: white;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
/* GitHub-style headings */
|
|
h1 {
|
|
color: #24292f;
|
|
font-size: 2em;
|
|
font-weight: 600;
|
|
margin: 0 0 16pt 0;
|
|
padding-bottom: 8pt;
|
|
border-bottom: 1px solid #d1d9e0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
h2 {
|
|
color: #24292f;
|
|
font-size: 1.5em;
|
|
font-weight: 600;
|
|
margin: 24pt 0 12pt 0;
|
|
padding-bottom: 6pt;
|
|
border-bottom: 1px solid #d1d9e0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
h3 {
|
|
color: #24292f;
|
|
font-size: 1.25em;
|
|
font-weight: 600;
|
|
margin: 18pt 0 8pt 0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
h4 {
|
|
color: #24292f;
|
|
font-size: 1em;
|
|
font-weight: 600;
|
|
margin: 16pt 0 6pt 0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
h5, h6 {
|
|
color: #24292f;
|
|
font-size: 0.875em;
|
|
font-weight: 600;
|
|
margin: 14pt 0 6pt 0;
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
/* Paragraphs */
|
|
p {
|
|
margin: 0 0 12pt 0;
|
|
orphans: 2;
|
|
widows: 2;
|
|
}
|
|
|
|
/* Lists */
|
|
ul, ol {
|
|
margin: 0 0 12pt 0;
|
|
padding-left: 2em;
|
|
}
|
|
|
|
ul {
|
|
list-style-type: disc;
|
|
}
|
|
|
|
ol {
|
|
list-style-type: decimal;
|
|
}
|
|
|
|
li {
|
|
margin: 0 0 4pt 0;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Tasks */
|
|
li.todo {
|
|
list-style: none;
|
|
margin-left: -2em;
|
|
padding-left: 1.5em;
|
|
}
|
|
|
|
li.todo-done {
|
|
list-style: none;
|
|
margin-left: -2em;
|
|
padding-left: 1.5em;
|
|
color: #656d76;
|
|
}
|
|
|
|
/* Text formatting */
|
|
strong {
|
|
font-weight: 600;
|
|
}
|
|
|
|
em {
|
|
font-style: italic;
|
|
}
|
|
|
|
del {
|
|
text-decoration: line-through;
|
|
color: #656d76;
|
|
}
|
|
|
|
/* Links */
|
|
a {
|
|
color: #0969da;
|
|
text-decoration: none;
|
|
}
|
|
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Blockquotes */
|
|
blockquote {
|
|
margin: 12pt 0;
|
|
padding: 0 12pt;
|
|
border-left: 4px solid #d1d9e0;
|
|
color: #656d76;
|
|
}
|
|
|
|
blockquote > :first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
blockquote > :last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* Horizontal rule */
|
|
hr {
|
|
margin: 24pt 0;
|
|
border: none;
|
|
border-top: 1px solid #d1d9e0;
|
|
height: 0;
|
|
}
|
|
|
|
/* GitHub-style tables */
|
|
table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin: 12pt 0;
|
|
font-size: 10.5pt;
|
|
border: 1px solid #d1d9e0;
|
|
}
|
|
|
|
th, td {
|
|
border: 1px solid #d1d9e0;
|
|
padding: 6pt 10pt;
|
|
text-align: left;
|
|
}
|
|
|
|
th {
|
|
background-color: #f6f8fa;
|
|
font-weight: 600;
|
|
}
|
|
|
|
tr:nth-child(even) {
|
|
background-color: #f6f8fa;
|
|
}
|
|
|
|
/* GitHub-style code */
|
|
code {
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
background-color: rgba(175, 184, 193, 0.2);
|
|
padding: 1pt 4pt;
|
|
border-radius: 3pt;
|
|
font-size: 85%;
|
|
}
|
|
|
|
pre {
|
|
background-color: #f6f8fa;
|
|
padding: 12pt;
|
|
border-radius: 6pt;
|
|
border: 1px solid #d1d9e0;
|
|
margin: 12pt 0;
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
font-size: 85%;
|
|
line-height: 1.45;
|
|
overflow: visible;
|
|
}
|
|
|
|
pre code {
|
|
background: transparent;
|
|
padding: 0;
|
|
border-radius: 0;
|
|
font-size: inherit;
|
|
}
|
|
|
|
/* Mermaid diagrams */
|
|
.mermaid {
|
|
text-align: center;
|
|
margin: 16pt 0;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
.mermaid svg {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
/* Avoid page breaks */
|
|
h1, h2, h3, h4, h5, h6 {
|
|
page-break-after: avoid;
|
|
}
|
|
|
|
p, li {
|
|
page-break-inside: avoid;
|
|
orphans: 2;
|
|
widows: 2;
|
|
}
|
|
|
|
pre, table {
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* Improve readability */
|
|
body {
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
${html}
|
|
</body>
|
|
</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 = `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title}</title>
|
|
<link rel="stylesheet" href="style.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="presentation-container">
|
|
<div class="presentation-header">
|
|
<h1>${title}</h1>
|
|
<div class="date">${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</div>
|
|
</div>
|
|
<div class="presentation-content markdown-body markdown-preview" id="content"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// Render markdown content first
|
|
const content = ${JSON.stringify(content)};
|
|
const contentDiv = document.getElementById('content');
|
|
contentDiv.innerHTML = marked.parse(content);
|
|
|
|
// Initialize Mermaid
|
|
mermaid.initialize({
|
|
startOnLoad: false,
|
|
theme: 'default',
|
|
securityLevel: 'loose'
|
|
});
|
|
|
|
// Process Mermaid diagrams
|
|
setTimeout(() => {
|
|
const mermaidBlocks = contentDiv.querySelectorAll('code.language-mermaid, pre code.language-mermaid');
|
|
|
|
mermaidBlocks.forEach((block, index) => {
|
|
try {
|
|
const mermaidCode = block.textContent;
|
|
const uniqueId = 'mermaid-' + Date.now() + '-' + index;
|
|
|
|
const mermaidDiv = document.createElement('div');
|
|
mermaidDiv.id = uniqueId;
|
|
mermaidDiv.className = 'mermaid';
|
|
mermaidDiv.textContent = mermaidCode;
|
|
|
|
const pre = block.closest('pre') || block;
|
|
pre.parentNode.replaceChild(mermaidDiv, pre);
|
|
|
|
mermaid.render(uniqueId + '-svg', mermaidCode).then(function(result) {
|
|
mermaidDiv.innerHTML = result.svg;
|
|
}).catch(function(err) {
|
|
console.warn('Mermaid rendering error:', err);
|
|
mermaidDiv.innerHTML = '<pre style="color: red;">Mermaid error: ' + err.message + '</pre>';
|
|
});
|
|
} catch (error) {
|
|
console.warn('Mermaid processing error:', error);
|
|
}
|
|
});
|
|
}, 200);
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
|
|
// 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; |