Spaces:
Running
Running
| import { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, Table, TableRow, TableCell, WidthType } from 'docx'; | |
| import PDFDocument from 'pdfkit'; | |
| import ExcelJS from 'exceljs'; | |
| // @ts-ignore | |
| import officegen from 'officegen'; | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| export interface DocumentContent { | |
| title?: string; | |
| content: string | any[]; | |
| metadata?: Record<string, any>; | |
| } | |
| export interface TableData { | |
| headers: string[]; | |
| rows: string[][]; | |
| } | |
| export interface SlideContent { | |
| title: string; | |
| content: string | string[]; | |
| bullets?: string[]; | |
| } | |
| export class DocumentGenerator { | |
| // Generate DOCX file | |
| static async generateDOCX(content: DocumentContent): Promise<Buffer> { | |
| const doc = new Document({ | |
| sections: [{ | |
| properties: {}, | |
| children: this.parseContentToParagraphs(content.content, content.title) | |
| }] | |
| }); | |
| return await Packer.toBuffer(doc); | |
| } | |
| private static parseContentToParagraphs(content: string | any, title?: string): Paragraph[] { | |
| const paragraphs: Paragraph[] = []; | |
| // Add title if provided | |
| if (title) { | |
| paragraphs.push(new Paragraph({ | |
| text: title, | |
| heading: HeadingLevel.HEADING_1, | |
| spacing: { after: 200 } | |
| })); | |
| } | |
| // Handle complex nested structures (like the letter example) | |
| if (typeof content === 'object' && content !== null) { | |
| // Check if it has sections array | |
| if (content.sections && Array.isArray(content.sections)) { | |
| for (const section of content.sections) { | |
| if (section.content) { | |
| // Add section content | |
| if (typeof section.content === 'string') { | |
| const lines = section.content.split('\n'); | |
| for (const line of lines) { | |
| if (line.trim()) { | |
| paragraphs.push(new Paragraph({ | |
| children: [new TextRun(line)], | |
| spacing: { after: 100 } | |
| })); | |
| } | |
| } | |
| } | |
| } | |
| // Handle paragraphs array in section | |
| if (section.paragraphs && Array.isArray(section.paragraphs)) { | |
| for (const para of section.paragraphs) { | |
| paragraphs.push(new Paragraph({ | |
| children: [new TextRun(para)], | |
| spacing: { after: 100 } | |
| })); | |
| } | |
| } | |
| } | |
| return paragraphs; | |
| } | |
| // Handle direct content field | |
| if (content.content) { | |
| return this.parseContentToParagraphs(content.content, title); | |
| } | |
| } | |
| if (typeof content === 'string') { | |
| const lines = content.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('# ')) { | |
| // H1 heading | |
| paragraphs.push(new Paragraph({ | |
| text: line.substring(2), | |
| heading: HeadingLevel.HEADING_1 | |
| })); | |
| } else if (line.startsWith('## ')) { | |
| // H2 heading | |
| paragraphs.push(new Paragraph({ | |
| text: line.substring(3), | |
| heading: HeadingLevel.HEADING_2 | |
| })); | |
| } else if (line.startsWith('### ')) { | |
| // H3 heading | |
| paragraphs.push(new Paragraph({ | |
| text: line.substring(4), | |
| heading: HeadingLevel.HEADING_3 | |
| })); | |
| } else if (line.startsWith('- ') || line.startsWith('* ')) { | |
| // Bullet point | |
| paragraphs.push(new Paragraph({ | |
| text: line.substring(2), | |
| bullet: { | |
| level: 0 | |
| } | |
| })); | |
| } else if (line.startsWith('1. ') || /^\d+\. /.test(line)) { | |
| // Numbered list | |
| paragraphs.push(new Paragraph({ | |
| text: line.replace(/^\d+\. /, ''), | |
| numbering: { | |
| reference: "default-numbering", | |
| level: 0 | |
| } | |
| })); | |
| } else { | |
| // Regular paragraph | |
| paragraphs.push(new Paragraph({ | |
| children: [new TextRun(line)] | |
| })); | |
| } | |
| } | |
| } else if (Array.isArray(content)) { | |
| for (const item of content) { | |
| if (typeof item === 'string') { | |
| paragraphs.push(new Paragraph({ | |
| children: [new TextRun(item)] | |
| })); | |
| } else if (item.type === 'heading') { | |
| paragraphs.push(new Paragraph({ | |
| text: item.text, | |
| heading: item.level || HeadingLevel.HEADING_1 | |
| })); | |
| } else if (item.type === 'paragraph') { | |
| paragraphs.push(new Paragraph({ | |
| children: [new TextRun(item.text)] | |
| })); | |
| } else if (item.type === 'bullet') { | |
| paragraphs.push(new Paragraph({ | |
| text: item.text, | |
| bullet: { | |
| level: item.level || 0 | |
| } | |
| })); | |
| } | |
| } | |
| } | |
| return paragraphs; | |
| } | |
| // Generate PDF file | |
| static async generatePDF(content: DocumentContent): Promise<Buffer> { | |
| return new Promise((resolve, reject) => { | |
| const doc = new PDFDocument(); | |
| const buffers: Buffer[] = []; | |
| doc.on('data', buffers.push.bind(buffers)); | |
| doc.on('end', () => { | |
| const pdfData = Buffer.concat(buffers); | |
| resolve(pdfData); | |
| }); | |
| doc.on('error', reject); | |
| // Add title if provided | |
| if (content.title) { | |
| doc.fontSize(20).text(content.title, { align: 'center' }); | |
| doc.moveDown(); | |
| } | |
| // Add content | |
| if (typeof content.content === 'string') { | |
| const lines = content.content.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('# ')) { | |
| doc.fontSize(18).text(line.substring(2)); | |
| } else if (line.startsWith('## ')) { | |
| doc.fontSize(16).text(line.substring(3)); | |
| } else if (line.startsWith('### ')) { | |
| doc.fontSize(14).text(line.substring(4)); | |
| } else if (line.startsWith('- ') || line.startsWith('* ')) { | |
| doc.fontSize(12).text(`• ${line.substring(2)}`, { indent: 20 }); | |
| } else { | |
| doc.fontSize(12).text(line); | |
| } | |
| doc.moveDown(0.5); | |
| } | |
| } | |
| doc.end(); | |
| }); | |
| } | |
| // Generate PowerPoint file | |
| static async generatePowerPoint(slides: SlideContent[]): Promise<Buffer> { | |
| return new Promise((resolve, reject) => { | |
| const pptx = officegen('pptx'); | |
| const buffers: Buffer[] = []; | |
| pptx.on('data', (data: Buffer) => buffers.push(data)); | |
| pptx.on('end', () => { | |
| const pptData = Buffer.concat(buffers); | |
| resolve(pptData); | |
| }); | |
| pptx.on('error', reject); | |
| // Add title slide | |
| if (slides.length > 0 && slides[0].title) { | |
| const titleSlide = pptx.makeNewSlide(); | |
| titleSlide.title = slides[0].title; | |
| if (slides[0].content) { | |
| titleSlide.addText(slides[0].content.toString(), { | |
| x: 'c', | |
| y: 'c', | |
| cx: '80%', | |
| cy: '30%', | |
| font_size: 18 | |
| }); | |
| } | |
| } | |
| // Add content slides | |
| for (let i = 1; i < slides.length; i++) { | |
| const slide = pptx.makeNewSlide(); | |
| slide.title = slides[i].title; | |
| if (slides[i].bullets && slides[i].bullets!.length > 0) { | |
| // Add bullet points | |
| const bullets = slides[i].bullets!.map(bullet => ({ | |
| text: bullet, | |
| options: { font_size: 14 } | |
| })); | |
| slide.addText(bullets, { | |
| x: 0.5, | |
| y: 1.5, | |
| cx: '90%', | |
| cy: '70%' | |
| }); | |
| } else if (slides[i].content) { | |
| // Add regular content | |
| slide.addText(slides[i].content.toString(), { | |
| x: 0.5, | |
| y: 1.5, | |
| cx: '90%', | |
| cy: '70%', | |
| font_size: 14 | |
| }); | |
| } | |
| } | |
| pptx.generate(); | |
| }); | |
| } | |
| // Generate Excel file | |
| static async generateExcel(data: { sheets: { name: string; data: TableData }[] }): Promise<Buffer> { | |
| const workbook = new ExcelJS.Workbook(); | |
| workbook.creator = 'ReubenOS'; | |
| workbook.created = new Date(); | |
| workbook.modified = new Date(); | |
| for (const sheetData of data.sheets) { | |
| const worksheet = workbook.addWorksheet(sheetData.name); | |
| // Add headers | |
| worksheet.addRow(sheetData.data.headers); | |
| // Style headers | |
| worksheet.getRow(1).font = { bold: true }; | |
| worksheet.getRow(1).fill = { | |
| type: 'pattern', | |
| pattern: 'solid', | |
| fgColor: { argb: 'FFE0E0E0' } | |
| }; | |
| // Add data rows | |
| for (const row of sheetData.data.rows) { | |
| worksheet.addRow(row); | |
| } | |
| // Auto-fit columns | |
| worksheet.columns.forEach((column) => { | |
| let maxLength = 0; | |
| if (column && column.eachCell) { | |
| column.eachCell({ includeEmpty: true }, (cell) => { | |
| const length = cell.value ? cell.value.toString().length : 10; | |
| if (length > maxLength) { | |
| maxLength = length; | |
| } | |
| }); | |
| column.width = maxLength + 2; | |
| } | |
| }); | |
| // Add borders to all cells with data | |
| const rowCount = worksheet.rowCount; | |
| const colCount = worksheet.columnCount; | |
| for (let row = 1; row <= rowCount; row++) { | |
| for (let col = 1; col <= colCount; col++) { | |
| const cell = worksheet.getCell(row, col); | |
| cell.border = { | |
| top: { style: 'thin' }, | |
| left: { style: 'thin' }, | |
| bottom: { style: 'thin' }, | |
| right: { style: 'thin' } | |
| }; | |
| } | |
| } | |
| } | |
| const buffer = await workbook.xlsx.writeBuffer(); | |
| return Buffer.from(buffer); | |
| } | |
| // Generate LaTeX and compile to PDF (requires latex installation) | |
| static async generateLatexPDF(latexContent: string): Promise<Buffer> { | |
| // For now, we'll use PDFKit as LaTeX compilation requires external tools | |
| // In production, you'd use node-latex or similar with a LaTeX installation | |
| return this.generatePDF({ | |
| content: this.convertLatexToPlainText(latexContent) | |
| }); | |
| } | |
| private static convertLatexToPlainText(latex: string): string { | |
| // Basic LaTeX to plain text conversion | |
| return latex | |
| .replace(/\\documentclass{.*?}/g, '') | |
| .replace(/\\usepackage{.*?}/g, '') | |
| .replace(/\\begin{document}/g, '') | |
| .replace(/\\end{document}/g, '') | |
| .replace(/\\section{(.*?)}/g, '# $1') | |
| .replace(/\\subsection{(.*?)}/g, '## $1') | |
| .replace(/\\subsubsection{(.*?)}/g, '### $1') | |
| .replace(/\\textbf{(.*?)}/g, '**$1**') | |
| .replace(/\\textit{(.*?)}/g, '*$1*') | |
| .replace(/\\item/g, '- ') | |
| .replace(/\\begin{itemize}/g, '') | |
| .replace(/\\end{itemize}/g, '') | |
| .replace(/\\begin{enumerate}/g, '') | |
| .replace(/\\end{enumerate}/g, '') | |
| .replace(/\$/g, '') | |
| .replace(/\\/g, '\n') | |
| .trim(); | |
| } | |
| // Convert markdown to structured content for document generation | |
| static parseMarkdown(markdown: string): any[] { | |
| const lines = markdown.split('\n'); | |
| const content = []; | |
| for (const line of lines) { | |
| if (line.startsWith('# ')) { | |
| content.push({ type: 'heading', text: line.substring(2), level: HeadingLevel.HEADING_1 }); | |
| } else if (line.startsWith('## ')) { | |
| content.push({ type: 'heading', text: line.substring(3), level: HeadingLevel.HEADING_2 }); | |
| } else if (line.startsWith('### ')) { | |
| content.push({ type: 'heading', text: line.substring(4), level: HeadingLevel.HEADING_3 }); | |
| } else if (line.startsWith('- ') || line.startsWith('* ')) { | |
| content.push({ type: 'bullet', text: line.substring(2) }); | |
| } else if (line.trim() !== '') { | |
| content.push({ type: 'paragraph', text: line }); | |
| } | |
| } | |
| return content; | |
| } | |
| } |