/** * vite-plugin-editor-api.js - Vite dev server plugin voor de Markdown editor * * Biedt API endpoints voor het opslaan van Markdown en het uploaden van afbeeldingen. * Alleen actief tijdens development (npm run dev), niet in productie builds. * * Endpoints: * - POST /__editor/save - Slaat Markdown content op naar content/installatie.md * - POST /__editor/upload - Upload een afbeelding naar public/images/installatie/ * - GET /__editor/images - Lijst beschikbare afbeeldingen */ /* eslint-disable no-undef */ import fs from 'fs'; import path from 'path'; export function editorApiPlugin() { return { name: 'editor-api', configureServer(server) { const rootDir = server.config.root; // POST /__editor/save - Markdown content opslaan server.middlewares.use('/__editor/save', (req, res, next) => { if (req.method !== 'POST') return next(); let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { try { const { content } = JSON.parse(body); const filePath = path.join(rootDir, 'content', 'installatie.md'); // Zorg dat de content directory bestaat const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, content, 'utf-8'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, path: filePath })); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); } }); }); // POST /__editor/upload - Afbeelding uploaden server.middlewares.use('/__editor/upload', (req, res, next) => { if (req.method !== 'POST') return next(); const chunks = []; req.on('data', chunk => chunks.push(chunk)); req.on('end', () => { try { const buffer = Buffer.concat(chunks); const boundary = req.headers['content-type'].split('boundary=')[1]; const { filename, fileData } = parseMultipart(buffer, boundary); if (!filename) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Geen bestand gevonden' })); return; } // Valideer bestandstype const ext = path.extname(filename).toLowerCase(); const allowed = ['.png', '.jpg', '.jpeg', '.gif', '.webp']; if (!allowed.includes(ext)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Bestandstype ${ext} niet toegestaan` })); return; } // Bepaal bestandsnaam (voeg timestamp toe bij duplicaat) const imageDir = path.join(rootDir, 'public', 'images', 'installatie'); if (!fs.existsSync(imageDir)) { fs.mkdirSync(imageDir, { recursive: true }); } let finalName = filename; const destPath = path.join(imageDir, finalName); if (fs.existsSync(destPath)) { const base = path.basename(filename, ext); const timestamp = Date.now(); finalName = `${base}-${timestamp}${ext}`; } fs.writeFileSync(path.join(imageDir, finalName), fileData); const relativePath = `images/installatie/${finalName}`; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, path: relativePath, filename: finalName })); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); } }); }); // GET /__editor/images - Lijst beschikbare afbeeldingen server.middlewares.use('/__editor/images', (req, res, next) => { if (req.method !== 'GET') return next(); try { const imageDir = path.join(rootDir, 'public', 'images', 'installatie'); if (!fs.existsSync(imageDir)) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ images: [] })); return; } const files = fs.readdirSync(imageDir) .filter(f => /\.(png|jpg|jpeg|gif|webp)$/i.test(f)) .map(f => ({ filename: f, path: `images/installatie/${f}`, })); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ images: files })); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); } }); }, }; } /** * Simpele multipart/form-data parser. * Extraheert het eerste bestand uit de multipart body. */ function parseMultipart(buffer, boundary) { const boundaryBuffer = Buffer.from(`--${boundary}`); const parts = []; let start = 0; while (true) { const idx = buffer.indexOf(boundaryBuffer, start); if (idx === -1) break; if (start > 0) { // Verwijder de trailing \r\n voor de boundary parts.push(buffer.slice(start, idx - 2)); } start = idx + boundaryBuffer.length + 2; // Skip boundary + \r\n } for (const part of parts) { const headerEnd = part.indexOf('\r\n\r\n'); if (headerEnd === -1) continue; const headers = part.slice(0, headerEnd).toString('utf-8'); const filenameMatch = headers.match(/filename="(.+?)"/); if (filenameMatch) { return { filename: filenameMatch[1], fileData: part.slice(headerEnd + 4), }; } } return { filename: null, fileData: null }; }