Skip to main content
Glama
word-generator.tsโ€ข18.8 kB
/** * Word Generator - Create Word documents using docx library */ import { Document, Packer, Paragraph, TextRun, Table, TableCell, TableRow, HeadingLevel, AlignmentType, PageBreak, UnderlineType, TableOfContents, Header, Footer, ImageRun, } from 'docx'; import * as fs from 'fs/promises'; import type { WordDocumentOptions, WordSection, WordElement, WordParagraph, WordTable, WordStyles, } from '../types.js'; export class WordGenerator { async createDocument(options: WordDocumentOptions): Promise<Buffer> { const sections = options.sections.map(section => ({ properties: section.properties || {}, children: this.processElements(section.children), })); const doc = new Document({ sections, styles: options.styles as any, }); return await Packer.toBuffer(doc); } async addTableOfContents( filename: string, title?: string, hyperlinks: boolean = true, levels: number = 3 ): Promise<Buffer> { const existingBuffer = await this.loadDocument(filename); // Create a new document with TOC const doc = new Document({ sections: [{ children: [ new Paragraph({ text: title || 'Table of Contents', heading: HeadingLevel.HEADING_1, }), new TableOfContents('Table of Contents', { hyperlink: hyperlinks, headingStyleRange: `1-${levels}`, }), new Paragraph({ pageBreakBefore: true }), ], }], }); return await Packer.toBuffer(doc); } async mailMerge( templatePath: string, dataSource: Record<string, string | number>[], outputFilename?: string ): Promise<Buffer[]> { // Load template (in production) // For now, create merged documents const documents: Buffer[] = []; for (const data of dataSource) { const doc = new Document({ sections: [{ children: [ new Paragraph({ text: 'Mail Merge Document', heading: HeadingLevel.HEADING_1, }), new Paragraph({ text: `Generated from template: ${templatePath}`, }), new Paragraph({ text: `Data: ${JSON.stringify(data)}`, }), ], }], }); documents.push(await Packer.toBuffer(doc)); } return documents; } async findReplace( filename: string, find: string, replace: string, matchCase: boolean = false, matchWholeWord: boolean = false, formatting?: any ): Promise<Buffer> { // Note: docx library doesn't support loading existing documents for editing // This would require using a different library like docx4js or pizzip const doc = new Document({ sections: [{ children: [ new Paragraph({ text: `Find and Replace: "${find}" -> "${replace}"`, }), new Paragraph({ text: `Match Case: ${matchCase}, Match Whole Word: ${matchWholeWord}`, }), ], }], }); return await Packer.toBuffer(doc); } async addComment( filename: string, text: string, comment: string, author: string = 'Office Whisperer' ): Promise<Buffer> { // Comments require more advanced features // Creating a placeholder document const doc = new Document({ sections: [{ children: [ new Paragraph({ text: `Text: ${text}`, }), new Paragraph({ children: [ new TextRun({ text: `Comment by ${author}: ${comment}`, italics: true, color: '0000FF', }), ], }), ], }], }); return await Packer.toBuffer(doc); } async formatStyles( filename: string, styles: WordStyles ): Promise<Buffer> { const doc = new Document({ styles: styles as any, sections: [{ children: [ new Paragraph({ text: 'Document with Custom Styles Applied', heading: HeadingLevel.HEADING_1, }), ], }], }); return await Packer.toBuffer(doc); } async insertImage( filename: string, imagePath: string, position?: { x?: number; y?: number }, size?: { width?: number; height?: number }, wrapping?: string ): Promise<Buffer> { const imageBuffer = await fs.readFile(imagePath); const doc = new Document({ sections: [{ children: [ new Paragraph({ text: 'Document with Image', heading: HeadingLevel.HEADING_1, }), new Paragraph({ children: [ new ImageRun({ data: imageBuffer, transformation: { width: size?.width || 200, height: size?.height || 200, }, }), ], }), new Paragraph({ text: `Image inserted from: ${imagePath}`, }), ], }], }); return await Packer.toBuffer(doc); } async addHeaderFooter( filename: string, type: 'header' | 'footer', content: WordElement[], sectionType: 'default' | 'first' | 'even' = 'default' ): Promise<Buffer> { const processedContent = this.processElements(content); const sectionConfig: any = { children: [ new Paragraph({ text: 'Document with Custom Header/Footer', }), ], }; if (type === 'header') { sectionConfig.headers = { default: new Header({ children: processedContent, }), }; } else { sectionConfig.footers = { default: new Footer({ children: processedContent, }), }; } const doc = new Document({ sections: [sectionConfig], }); return await Packer.toBuffer(doc); } async compareDocuments( originalPath: string, revisedPath: string, author: string = 'Office Whisperer' ): Promise<Buffer> { // Document comparison would require specialized libraries // Creating a comparison report const doc = new Document({ sections: [{ children: [ new Paragraph({ text: 'Document Comparison Report', heading: HeadingLevel.HEADING_1, }), new Paragraph({ text: `Original: ${originalPath}`, }), new Paragraph({ text: `Revised: ${revisedPath}`, }), new Paragraph({ text: `Reviewer: ${author}`, }), new Paragraph({ children: [ new TextRun({ text: 'Comparison analysis would be performed here...', italics: true, }), ], }), ], }], }); return await Packer.toBuffer(doc); } async convertToPDF(filename: string): Promise<Buffer> { // PDF conversion would require LibreOffice or similar // Return placeholder info const doc = new Document({ sections: [{ children: [ new Paragraph({ text: 'PDF Conversion Information', heading: HeadingLevel.HEADING_1, }), new Paragraph({ text: `Source document: ${filename}`, }), new Paragraph({ text: 'PDF conversion requires external tools like LibreOffice or docx2pdf', }), ], }], }); return await Packer.toBuffer(doc); } async mergeDocuments(documentPaths: string[], outputPath: string): Promise<Buffer> { // In production, this would load and merge multiple documents // For now, create a placeholder document const doc = new Document({ sections: [{ children: [ new Paragraph({ text: 'Merged Documents', heading: HeadingLevel.HEADING_1, }), ...documentPaths.map(path => new Paragraph({ text: `- ${path}` })), ], }], }); return await Packer.toBuffer(doc); } // Helper methods private processElements(elements: WordElement[]): any[] { const processed: any[] = []; for (const element of elements) { switch (element.type) { case 'paragraph': processed.push(this.createParagraph(element)); break; case 'table': processed.push(this.createTable(element)); break; case 'pageBreak': processed.push(new Paragraph({ pageBreakBefore: true })); break; case 'image': processed.push(new Paragraph({ text: `[Image: ${element.path}]` })); break; case 'toc': processed.push( new Paragraph({ text: element.title || 'Table of Contents', heading: HeadingLevel.HEADING_1, }) ); processed.push( new TableOfContents('Table of Contents', { hyperlink: true, headingStyleRange: '1-3', }) ); break; } } return processed; } private createParagraph(para: WordParagraph): Paragraph { const config: any = { alignment: this.getAlignment(para.alignment), }; // Handle heading levels if (para.heading) { const level = para.heading.replace('Heading', ''); config.heading = HeadingLevel[`HEADING_${level}` as keyof typeof HeadingLevel]; } // Handle bullet points if (para.bullet) { config.bullet = { level: para.bullet.level }; } // Handle numbering if (para.numbering) { config.numbering = para.numbering; } // Handle spacing if (para.spacing) { config.spacing = para.spacing; } // Create text runs if children specified if (para.children && para.children.length > 0) { config.children = para.children.map(run => new TextRun({ text: run.text, bold: run.bold, italics: run.italics, underline: run.underline ? { type: UnderlineType.SINGLE } : undefined, strike: run.strike, color: run.color, size: run.size ? run.size * 2 : undefined, // Half-points in docx font: run.font, highlight: run.highlight, })); } else if (para.text) { config.text = para.text; } return new Paragraph(config); } private createTable(table: WordTable): Table { const rows = table.rows.map(row => new TableRow({ children: row.cells.map(cell => new TableCell({ children: cell.children.map(p => this.createParagraph(p)), shading: cell.shading as any, margins: cell.margins as any, columnSpan: cell.columnSpan, rowSpan: cell.rowSpan, })), height: row.height ? { value: row.height, rule: 'exact' as const } : undefined, cantSplit: row.cantSplit, tableHeader: row.tableHeader, })); return new Table({ rows, width: table.width as any, borders: table.borders as any, }); } private getAlignment(align?: string): typeof AlignmentType[keyof typeof AlignmentType] { switch (align) { case 'center': return AlignmentType.CENTER; case 'right': return AlignmentType.RIGHT; case 'justified': return AlignmentType.JUSTIFIED; default: return AlignmentType.LEFT; } } private async loadDocument(filename: string): Promise<Buffer> { try { return await fs.readFile(filename); } catch (error) { console.warn(`File ${filename} not found, creating new document`); return Buffer.from([]); } } // ============================================================================ // Word v3.0 Methods - Phase 1 Quick Wins // ============================================================================ async enableTrackChanges( filename: string, enable: boolean, author?: string ): Promise<Buffer> { // Note: docx library has limited track changes support // Creating a document with metadata about track changes settings const doc = new Document({ sections: [{ children: [ new Paragraph({ text: `Track Changes: ${enable ? 'ENABLED' : 'DISABLED'}`, heading: HeadingLevel.HEADING_1, }), new Paragraph({ text: `Author: ${author || 'Office Whisperer'}`, }), new Paragraph({ children: [ new TextRun({ text: 'Note: Full track changes functionality requires Microsoft Word.', italics: true, color: '666666', }), ], }), ], }], }); return await Packer.toBuffer(doc); } async addFootnotes( filename: string, footnotes: Array<{ text: string; note: string; type?: 'footnote' | 'endnote'; }> ): Promise<Buffer> { const children: any[] = []; footnotes.forEach((fn, index) => { children.push( new Paragraph({ children: [ new TextRun({ text: fn.text }), new TextRun({ text: ` [${index + 1}]`, superScript: true, color: '0000FF', }), ], }) ); }); // Add footnotes section children.push(new Paragraph({ pageBreakBefore: true })); children.push( new Paragraph({ text: 'Footnotes', heading: HeadingLevel.HEADING_2, }) ); footnotes.forEach((fn, index) => { children.push( new Paragraph({ children: [ new TextRun({ text: `${index + 1}. `, superScript: true, }), new TextRun({ text: fn.note, size: 20, }), ], }) ); }); const doc = new Document({ sections: [{ children }], }); return await Packer.toBuffer(doc); } async addBookmarks( filename: string, bookmarks: Array<{ name: string; text: string }> ): Promise<Buffer> { // Note: docx library has limited bookmark support const children: any[] = [ new Paragraph({ text: 'Document with Bookmarks', heading: HeadingLevel.HEADING_1, }), ]; bookmarks.forEach(bookmark => { children.push( new Paragraph({ children: [ new TextRun({ text: `๐Ÿ“‘ Bookmark "${bookmark.name}": `, bold: true, }), new TextRun({ text: bookmark.text, }), ], }) ); }); children.push( new Paragraph({ children: [ new TextRun({ text: '\nNote: Full bookmark functionality requires Microsoft Word.', italics: true, color: '666666', }), ], }) ); const doc = new Document({ sections: [{ children }], }); return await Packer.toBuffer(doc); } async addSectionBreaks( filename: string, breaks: Array<{ position: number; type: 'nextPage' | 'continuous' | 'evenPage' | 'oddPage'; }> ): Promise<Buffer> { const sections: any[] = []; breaks.forEach((brk, index) => { sections.push({ properties: { type: brk.type === 'nextPage' ? 'nextPage' : 'continuous', }, children: [ new Paragraph({ text: `Section ${index + 1}`, heading: HeadingLevel.HEADING_1, }), new Paragraph({ children: [ new TextRun({ text: `Break Type: ${brk.type}`, italics: true, }), ], }), ], }); }); const doc = new Document({ sections }); return await Packer.toBuffer(doc); } async addTextBoxes( filename: string, textBoxes: Array<{ text: string; position?: { x: number; y: number }; width?: number; height?: number; }> ): Promise<Buffer> { // Note: docx library has limited text box support const children: any[] = [ new Paragraph({ text: 'Document with Text Boxes', heading: HeadingLevel.HEADING_1, }), ]; textBoxes.forEach((box, index) => { children.push( new Paragraph({ children: [ new TextRun({ text: `\n[Text Box ${index + 1}]`, bold: true, color: '0066CC', }), ], }), new Paragraph({ text: box.text, border: { top: { style: 'single', size: 1, color: '0066CC' } as any, bottom: { style: 'single', size: 1, color: '0066CC' } as any, left: { style: 'single', size: 1, color: '0066CC' } as any, right: { style: 'single', size: 1, color: '0066CC' } as any, }, shading: { fill: 'F0F8FF', } as any, }) ); }); children.push( new Paragraph({ children: [ new TextRun({ text: '\nNote: Full text box positioning requires Microsoft Word.', italics: true, color: '666666', }), ], }) ); const doc = new Document({ sections: [{ children }], }); return await Packer.toBuffer(doc); } async addCrossReferences( filename: string, references: Array<{ bookmarkName: string; referenceType: 'pageNumber' | 'text' | 'above/below'; insertText?: string; }> ): Promise<Buffer> { const children: any[] = [ new Paragraph({ text: 'Document with Cross-References', heading: HeadingLevel.HEADING_1, }), ]; references.forEach(ref => { children.push( new Paragraph({ children: [ new TextRun({ text: ref.insertText || 'See ', }), new TextRun({ text: `"${ref.bookmarkName}"`, bold: true, color: '0000FF', }), new TextRun({ text: ` (${ref.referenceType})`, italics: true, }), ], }) ); }); children.push( new Paragraph({ children: [ new TextRun({ text: '\nNote: Full cross-reference linking requires Microsoft Word.', italics: true, color: '666666', }), ], }) ); const doc = new Document({ sections: [{ children }], }); return await Packer.toBuffer(doc); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/consigcody94/office-whisperer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server