workshopclaudecode/vite-plugin-editor-api.js

170 lines
5.8 KiB
JavaScript
Raw Normal View History

/**
* 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 };
}