| import express from "express"; |
| import { Document, Packer, Paragraph, HeadingLevel, TextRun, AlignmentType, BorderStyle } from "docx"; |
| import PptxGenJS from "pptxgenjs"; |
| import ExcelJS from "exceljs"; |
| import PDFDocument from "pdfkit"; |
|
|
| const router = express.Router(); |
| const PURPLE = "7C3AED"; |
| const LIGHT = "F3E8FF"; |
|
|
| function sendBuffer(res, buf, filename, mime) { |
| res.setHeader("Content-Type", mime); |
| res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`); |
| res.send(buf); |
| } |
|
|
| |
| |
| function normalizeBlocks(content) { |
| const blocks = []; |
| if (!content) return blocks; |
| if (typeof content === "string") { blocks.push({ type: "p", text: content }); return blocks; } |
| if (content.heading) blocks.push({ type: "h", text: content.heading }); |
| |
| (content.paragraphs || []).forEach(p => blocks.push({ type: "p", text: asText(p) })); |
| |
| (content.sections || []).forEach(s => { |
| if (s.title) blocks.push({ type: "h", text: s.title }); |
| const body = s.body || s.content || s.text; |
| if (body) blocks.push({ type: "p", text: asText(body) }); |
| (s.bullets || s.points || []).forEach(b => blocks.push({ type: "li", text: asText(b) })); |
| }); |
| |
| (content.bullets || content.points || []).forEach(b => blocks.push({ type: "li", text: asText(b) })); |
| return blocks; |
| } |
| function asText(v) { |
| if (v == null) return ""; |
| if (typeof v === "string") return v; |
| if (Array.isArray(v)) return v.map(asText).join("\n"); |
| if (typeof v === "object") return v.text || v.body || v.content || JSON.stringify(v); |
| return String(v); |
| } |
| |
| function normalizeSlides(content, fallbackTitle) { |
| let slides = []; |
| if (content?.slides?.length) { |
| slides = content.slides.map(s => ({ |
| title: s.title || s.heading || fallbackTitle || "", |
| bullets: normalizeBullets(s), |
| })); |
| } else { |
| |
| const secs = content?.sections || []; |
| if (secs.length) { |
| slides = secs.map(s => ({ title: s.title || "", bullets: normalizeBullets(s) })); |
| } else { |
| slides = [{ title: fallbackTitle || "Slide", bullets: (content?.paragraphs || []).map(asText) }]; |
| } |
| } |
| return slides.filter(s => s.title || (s.bullets && s.bullets.length)); |
| } |
| function normalizeBullets(s) { |
| if (Array.isArray(s.bullets)) return s.bullets.map(asText); |
| if (Array.isArray(s.points)) return s.points.map(asText); |
| const body = s.body || s.content || s.text; |
| if (Array.isArray(body)) return body.map(asText); |
| if (typeof body === "string") return body.split(/\n|•/).map(x => x.trim()).filter(Boolean); |
| return []; |
| } |
|
|
| |
| async function buildDocx(title, content) { |
| const blocks = normalizeBlocks(content); |
| const children = [ |
| new Paragraph({ |
| alignment: AlignmentType.CENTER, |
| spacing: { after: 300 }, |
| border: { bottom: { color: PURPLE, space: 6, style: BorderStyle.SINGLE, size: 18 } }, |
| children: [new TextRun({ text: title || "Document", bold: true, size: 48, color: PURPLE })], |
| }), |
| ]; |
| for (const b of blocks) { |
| if (b.type === "h") |
| children.push(new Paragraph({ spacing: { before: 200, after: 100 }, children: [new TextRun({ text: b.text, bold: true, size: 30, color: PURPLE })] })); |
| else if (b.type === "li") |
| children.push(new Paragraph({ bullet: { level: 0 }, spacing: { after: 60 }, children: [new TextRun({ text: b.text, size: 24 })] })); |
| else |
| children.push(new Paragraph({ spacing: { after: 120 }, children: [new TextRun({ text: b.text, size: 24 })] })); |
| } |
| if (blocks.length === 0) children.push(new Paragraph({ children: [new TextRun(asText(content))] })); |
| const doc = new Document({ sections: [{ children }] }); |
| return Packer.toBuffer(doc); |
| } |
|
|
| |
| async function buildPptx(title, content) { |
| const pptx = new PptxGenJS(); |
| pptx.defineLayout({ name: "WIDE", width: 13.333, height: 7.5 }); |
| pptx.layout = "WIDE"; |
|
|
| |
| pptx.defineSlideMaster({ |
| title: "GENISI", |
| background: { color: "FFFFFF" }, |
| objects: [ |
| { rect: { x: 0, y: 0, w: 13.333, h: 0.25, fill: { color: PURPLE } } }, |
| { rect: { x: 0, y: 7.25, w: 13.333, h: 0.25, fill: { color: "C084FC" } } }, |
| ], |
| }); |
|
|
| |
| const cover = pptx.addSlide({ masterName: "GENISI" }); |
| cover.background = { color: PURPLE }; |
| cover.addShape(pptx.ShapeType.ellipse, { x: 10.5, y: -1.5, w: 4, h: 4, fill: { color: "9333EA" } }); |
| cover.addShape(pptx.ShapeType.ellipse, { x: -1, y: 5, w: 3.5, h: 3.5, fill: { color: "A855F7" } }); |
| cover.addText(title || "Presentation", { x: 0.8, y: 2.8, w: 11.7, h: 1.5, fontSize: 44, bold: true, color: "FFFFFF", align: "center" }); |
| cover.addText("Genisi", { x: 0.8, y: 4.4, w: 11.7, h: 0.6, fontSize: 18, color: "E9D5FF", align: "center" }); |
|
|
| const slides = normalizeSlides(content, title); |
| slides.forEach((s, idx) => { |
| const slide = pptx.addSlide({ masterName: "GENISI" }); |
| |
| slide.addShape(pptx.ShapeType.roundRect, { x: 0.5, y: 0.5, w: 0.5, h: 0.9, fill: { color: PURPLE }, rectRadius: 0.1 }); |
| slide.addText(s.title || `Slide ${idx + 1}`, { x: 1.2, y: 0.5, w: 11, h: 0.9, fontSize: 30, bold: true, color: PURPLE }); |
| |
| const bullets = (s.bullets || []).map(b => ({ text: b, options: { bullet: { code: "2022" }, fontSize: 20, color: "1F2937", paraSpaceAfter: 10 } })); |
| if (bullets.length) { |
| slide.addText(bullets, { x: 1.2, y: 1.7, w: 11, h: 5, valign: "top" }); |
| } |
| |
| slide.transition = { type: "fade", duration: 0.6 }; |
| slide.addText(`${idx + 1}`, { x: 12.4, y: 6.7, w: 0.7, h: 0.4, fontSize: 12, color: "9CA3AF", align: "right" }); |
| }); |
|
|
| const data = await pptx.write("nodebuffer"); |
| return Buffer.isBuffer(data) ? data : Buffer.from(data); |
| } |
|
|
| |
| async function buildXlsx(title, content) { |
| const wb = new ExcelJS.Workbook(); |
| wb.creator = "Genisi"; |
| const sheets = content?.sheets?.length |
| ? content.sheets |
| : [{ name: title || "Sheet1", headers: content?.headers || [], rows: content?.rows || [] }]; |
| sheets.forEach(sh => { |
| const ws = wb.addWorksheet((sh.name || "Sheet").slice(0, 31)); |
| if (sh.headers?.length) { |
| const hr = ws.addRow(sh.headers); |
| hr.eachCell(c => { |
| c.font = { bold: true, color: { argb: "FFFFFFFF" } }; |
| c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF7C3AED" } }; |
| c.alignment = { horizontal: "center", vertical: "middle" }; |
| c.border = { bottom: { style: "thin", color: { argb: "FFCCCCCC" } } }; |
| }); |
| } |
| (sh.rows || []).forEach((r, i) => { |
| const row = ws.addRow(r); |
| if (i % 2) row.eachCell(c => (c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFF6F0FF" } })); |
| }); |
| ws.columns.forEach(c => (c.width = 22)); |
| }); |
| const buf = await wb.xlsx.writeBuffer(); |
| return Buffer.from(buf); |
| } |
|
|
| function buildCsv(content) { |
| const headers = content?.headers || content?.sheets?.[0]?.headers || []; |
| const rows = content?.rows || content?.sheets?.[0]?.rows || []; |
| const esc = v => `"${String(v ?? "").replace(/"/g, '""')}"`; |
| const lines = []; |
| if (headers.length) lines.push(headers.map(esc).join(",")); |
| rows.forEach(r => lines.push(r.map(esc).join(","))); |
| return Buffer.from("\uFEFF" + lines.join("\n"), "utf8"); |
| } |
| |
| /* ---- PDF (تصميم جميل) ---- */ |
| function buildPdf(title, content) { |
| return new Promise((resolve, reject) => { |
| const doc = new PDFDocument({ margin: 54, size: "A4" }); |
| const chunks = []; |
| doc.on("data", c => chunks.push(c)); |
| doc.on("end", () => resolve(Buffer.concat(chunks))); |
| doc.on("error", reject); |
| |
| // شريط علوي |
| doc.rect(0, 0, doc.page.width, 70).fill("#7C3AED"); |
| doc.fillColor("#fff").fontSize(24).text(title || "Document", 54, 26, { width: doc.page.width - 108 }); |
| doc.moveDown(2).fillColor("#000"); |
| |
| const blocks = normalizeBlocks(content); |
| if (blocks.length === 0) { doc.fontSize(12).text(asText(content)); doc.end(); return; } |
| let y = 100; |
| doc.y = y; |
| for (const b of blocks) { |
| if (b.type === "h") { doc.moveDown(0.6).fontSize(15).fillColor("#7C3AED").text(b.text); doc.fillColor("#000"); } |
| else if (b.type === "li") { doc.fontSize(12).fillColor("#000").text("• " + b.text, { indent: 12 }); } |
| else { doc.fontSize(12).fillColor("#000").text(b.text); } |
| doc.moveDown(0.3); |
| } |
| doc.end(); |
| }); |
| } |
| |
| router.post("/document", async (req, res) => { |
| try { |
| const { format = "pdf", title = "Genisi Document", content = {} } = req.body; |
| const safe = (title || "document").replace(/[^\w\u0600-\u06FF\- ]/g, "_").slice(0, 60); |
| switch (format) { |
| case "docx": return sendBuffer(res, await buildDocx(title, content), `${safe}.docx`, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); |
| case "pptx": return sendBuffer(res, await buildPptx(title, content), `${safe}.pptx`, "application/vnd.openxmlformats-officedocument.presentationml.presentation"); |
| case "xlsx": return sendBuffer(res, await buildXlsx(title, content), `${safe}.xlsx`, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); |
| case "csv": return sendBuffer(res, buildCsv(content), `${safe}.csv`, "text/csv; charset=utf-8"); |
| case "pdf": |
| default: return sendBuffer(res, await buildPdf(title, content), `${safe}.pdf`, "application/pdf"); |
| } |
| } catch (err) { |
| console.error("document error:", err); |
| res.status(500).json({ error: "document_failed", message: String(err?.message || err) }); |
| } |
| }); |
| |
| export default router; |
| |