import { Document, Packer, Paragraph, Table, TableCell, TableRow, TextRun, HeadingLevel, WidthType, BorderStyle } from "docx";
import * as fs from "fs";
import * as path from "path";
export interface ReportSection {
heading: string;
content?: string;
table?: {
headers: string[];
rows: string[][];
};
}
export interface ReportData {
filename: string;
title: string;
sections: ReportSection[];
}
export class WordGenerator {
private outputDir: string;
constructor(outputDir: string = "./output") {
this.outputDir = outputDir;
// Ensure output directory exists
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
}
}
public async generateReport(data: ReportData): Promise<string> {
const doc = new Document({
sections: [
{
children: [
new Paragraph({
text: data.title,
heading: HeadingLevel.TITLE,
spacing: {
after: 200,
},
}),
...this.renderSections(data.sections),
],
},
],
});
const buffer = await Packer.toBuffer(doc);
const fileNameWithExt = data.filename.endsWith(".docx")
? data.filename
: `${data.filename}.docx`;
const filePath = path.join(this.outputDir, fileNameWithExt);
fs.writeFileSync(filePath, buffer);
return filePath;
}
private renderSections(sections: ReportSection[]): (Paragraph | Table)[] {
const elements: (Paragraph | Table)[] = [];
for (const section of sections) {
// detailed heading
elements.push(
new Paragraph({
text: section.heading,
heading: HeadingLevel.HEADING_1,
spacing: {
before: 200,
after: 100,
},
})
);
// plain content (basic markdown support: splitting by lines)
if (section.content) {
const lines = section.content.split("\n");
for (const line of lines) {
// Basic bold handling for **text**
// This is a very primitive parser.
const textRuns = this.parseMarkdownLine(line);
elements.push(
new Paragraph({
children: textRuns,
spacing: {
after: 100
}
})
);
}
}
// table
if (section.table) {
elements.push(this.createTable(section.table.headers, section.table.rows));
}
}
return elements;
}
private parseMarkdownLine(line: string): TextRun[] {
// Very basic bold parser: **text**
const parts = line.split(/(\*\*.*?\*\*)/);
return parts.map(part => {
if (part.startsWith("**") && part.endsWith("**")) {
return new TextRun({
text: part.slice(2, -2),
bold: true
});
}
return new TextRun({ text: part });
});
}
private createTable(headers: string[], rows: string[][]): Table {
// Create header row
const headerRow = new TableRow({
children: headers.map(
(header) =>
new TableCell({
children: [new Paragraph({ children: [new TextRun({ text: header, bold: true })] })],
width: {
size: 100 / headers.length,
type: WidthType.PERCENTAGE,
}
})
),
});
// Create data rows
const dataRows = rows.map(
(row) =>
new TableRow({
children: row.map(
(cell) =>
new TableCell({
children: [new Paragraph(cell)],
width: {
size: 100 / headers.length, // Distribute evenly for now
type: WidthType.PERCENTAGE,
}
})
),
})
);
return new Table({
rows: [headerRow, ...dataRows],
width: {
size: 100,
type: WidthType.PERCENTAGE
},
borders: {
top: { style: BorderStyle.SINGLE, size: 1 },
bottom: { style: BorderStyle.SINGLE, size: 1 },
left: { style: BorderStyle.SINGLE, size: 1 },
right: { style: BorderStyle.SINGLE, size: 1 },
insideHorizontal: { style: BorderStyle.SINGLE, size: 1 },
insideVertical: { style: BorderStyle.SINGLE, size: 1 },
}
});
}
}